From 658a6ca7bbdba15d0f54cff65f05f3b58839d8ef Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sun, 24 Nov 2024 15:05:15 -0500 Subject: [PATCH 01/13] build traefik config for newt correctly --- server/routers/traefik/getTraefikConfig.ts | 36 ++++++++++++++++------ 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 1051659a..5ed9958f 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -22,6 +22,10 @@ export async function traefikConfigProvider( schema.orgs, eq(schema.resources.orgId, schema.orgs.orgId), ) + .innerJoin( + schema.sites, + eq(schema.sites.siteId, schema.resources.siteId), + ) .where( and( eq(schema.targets.enabled, true), @@ -70,6 +74,7 @@ export async function traefikConfigProvider( for (const item of all) { const target = item.targets; const resource = item.resources; + const site = item.sites; const org = item.orgs; const routerName = `${target.targetId}-router`; @@ -128,15 +133,28 @@ export async function traefikConfigProvider( }; } - http.services![serviceName] = { - loadBalancer: { - servers: [ - { - url: `${target.method}://${target.ip}:${target.port}`, - }, - ], - }, - }; + if (site.type === "newt") { + const ip = site.subnet.split("/")[0]; + http.services![serviceName] = { + loadBalancer: { + servers: [ + { + url: `${target.method}://${ip}:${target.internalPort}`, + }, + ], + }, + }; + } else if (site.type === "wireguard") { + http.services![serviceName] = { + loadBalancer: { + servers: [ + { + url: `${target.method}://${target.ip}:${target.port}`, + }, + ], + }, + }; + } } return res.status(HttpCode.OK).json({ http }); From ce2bfcddd5b68f6dff97e3db4fea4c4d20b42ee3 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sun, 24 Nov 2024 22:34:11 -0500 Subject: [PATCH 02/13] lighten dark background, add more info to resources table --- config.example.yml | 1 + server/routers/badger/verifySession.ts | 4 +- server/routers/resource/createResource.ts | 1 + server/routers/resource/listResources.ts | 67 +++++++---- server/routers/traefik/getTraefikConfig.ts | 2 +- src/app/[orgId]/settings/layout.tsx | 2 +- .../resources/components/ResourcesTable.tsx | 107 +++++++++++++++++- src/app/[orgId]/settings/resources/page.tsx | 12 +- .../settings/sites/components/SitesTable.tsx | 4 +- src/app/[orgId]/settings/sites/page.tsx | 16 ++- src/app/globals.css | 22 ++-- 11 files changed, 191 insertions(+), 47 deletions(-) diff --git a/config.example.yml b/config.example.yml index f84b60a9..0c8ed323 100644 --- a/config.example.yml +++ b/config.example.yml @@ -14,6 +14,7 @@ traefik: cert_resolver: letsencrypt http_entrypoint: web https_entrypoint: websecure + prefer_wildcard_cert: true badger: session_query_parameter: __pang_sess diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index a733c01e..cd8529a1 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -165,7 +165,7 @@ function notAllowed(res: Response, redirectUrl?: string) { error: false, message: "Access denied", status: HttpCode.OK, - } + }; logger.debug(JSON.stringify(data)); return response(res, data); } @@ -177,7 +177,7 @@ function allowed(res: Response) { error: false, message: "Access allowed", status: HttpCode.OK, - } + }; logger.debug(JSON.stringify(data)); return response(res, data); } diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 50473fad..002e5434 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -94,6 +94,7 @@ export async function createResource( orgId, name, subdomain, + ssl: true, }) .returning(); diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 10f19a8e..14b7c4b4 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -6,6 +6,8 @@ import { sites, userResources, roleResources, + resourcePassword, + resourcePincode, } from "@server/db/schema"; import response from "@server/utils/response"; import HttpCode from "@server/types/HttpCode"; @@ -46,39 +48,63 @@ const listResourcesSchema = z.object({ function queryResources( accessibleResourceIds: number[], siteId?: number, - orgId?: string + orgId?: string, ) { if (siteId) { return db .select({ resourceId: resources.resourceId, name: resources.name, - subdomain: resources.subdomain, + fullDomain: resources.fullDomain, + ssl: resources.ssl, siteName: sites.name, + passwordId: resourcePassword.passwordId, + pincodeId: resourcePincode.pincodeId, + sso: resources.sso, }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId), + ) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId), + ) .where( and( inArray(resources.resourceId, accessibleResourceIds), - eq(resources.siteId, siteId) - ) + eq(resources.siteId, siteId), + ), ); } else if (orgId) { return db .select({ resourceId: resources.resourceId, name: resources.name, - subdomain: resources.subdomain, + ssl: resources.ssl, + fullDomain: resources.fullDomain, siteName: sites.name, + passwordId: resourcePassword.passwordId, + sso: resources.sso, + pincodeId: resourcePincode.pincodeId, }) .from(resources) .leftJoin(sites, eq(resources.siteId, sites.siteId)) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId), + ) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId), + ) .where( and( inArray(resources.resourceId, accessibleResourceIds), - eq(resources.orgId, orgId) - ) + eq(resources.orgId, orgId), + ), ); } } @@ -91,7 +117,7 @@ export type ListResourcesResponse = { export async function listResources( req: Request, res: Response, - next: NextFunction + next: NextFunction, ): Promise { try { const parsedQuery = listResourcesSchema.safeParse(req.query); @@ -99,8 +125,8 @@ export async function listResources( return next( createHttpError( HttpCode.BAD_REQUEST, - parsedQuery.error.errors.map((e) => e.message).join(", ") - ) + parsedQuery.error.errors.map((e) => e.message).join(", "), + ), ); } const { limit, offset } = parsedQuery.data; @@ -110,8 +136,8 @@ export async function listResources( return next( createHttpError( HttpCode.BAD_REQUEST, - parsedParams.error.errors.map((e) => e.message).join(", ") - ) + parsedParams.error.errors.map((e) => e.message).join(", "), + ), ); } const { siteId, orgId } = parsedParams.data; @@ -120,8 +146,8 @@ export async function listResources( return next( createHttpError( HttpCode.FORBIDDEN, - "User does not have access to this organization" - ) + "User does not have access to this organization", + ), ); } @@ -132,17 +158,17 @@ export async function listResources( .from(userResources) .fullJoin( roleResources, - eq(userResources.resourceId, roleResources.resourceId) + eq(userResources.resourceId, roleResources.resourceId), ) .where( or( eq(userResources.userId, req.user!.userId), - eq(roleResources.roleId, req.userOrgRoleId!) - ) + eq(roleResources.roleId, req.userOrgRoleId!), + ), ); const accessibleResourceIds = accessibleResources.map( - (resource) => resource.resourceId + (resource) => resource.resourceId, ); let countQuery: any = db @@ -173,7 +199,10 @@ export async function listResources( } catch (error) { logger.error(error); return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred", + ), ); } } diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 5ed9958f..dc2ecb56 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -117,7 +117,7 @@ export async function traefikConfigProvider( ? config.traefik.https_entrypoint : config.traefik.http_entrypoint, ], - middlewares: resource.ssl ? [badgerMiddlewareName] : [], + middlewares: [badgerMiddlewareName], service: serviceName, rule: `Host(\`${fullDomain}\`)`, ...(resource.ssl ? { tls } : {}), diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index b6ea8448..45095896 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -91,7 +91,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( <> -
+
); }, + cell: ({ row }) => { + const resourceRow = row.original; + return ( + + ); + }, }, { accessorKey: "domain", header: "Domain", + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ + {resourceRow.domain} + + +
+ ); + }, + }, + { + accessorKey: "hasAuth", + header: "Authentication", + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ {resourceRow.hasAuth ? ( + + + Protected + + ) : ( + + + Not Protected + + )} +
+ ); + }, }, { id: "actions", @@ -130,11 +229,11 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { @@ -146,7 +245,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { className="ml-2" onClick={() => router.push( - `/${resourceRow.orgId}/settings/resources/${resourceRow.id}` + `/${resourceRow.orgId}/settings/resources/${resourceRow.id}`, ) } > diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index fdd30487..e9aa41c9 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -19,7 +19,7 @@ export default async function ResourcesPage(props: ResourcesPageProps) { try { const res = await internal.get>( `/org/${params.orgId}/resources`, - await authCookieHeader() + await authCookieHeader(), ); resources = res.data.data.resources; } catch (e) { @@ -31,8 +31,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) { const getOrg = cache(async () => internal.get>( `/org/${params.orgId}`, - await authCookieHeader() - ) + await authCookieHeader(), + ), ); const res = await getOrg(); org = res.data.data; @@ -49,8 +49,12 @@ export default async function ResourcesPage(props: ResourcesPageProps) { id: resource.resourceId, name: resource.name, orgId: params.orgId, - domain: resource.subdomain || "", + domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`, site: resource.siteName || "None", + hasAuth: + resource.sso || + resource.pincodeId !== null || + resource.pincodeId !== null, }; }); diff --git a/src/app/[orgId]/settings/sites/components/SitesTable.tsx b/src/app/[orgId]/settings/sites/components/SitesTable.tsx index 124f952e..277dadb0 100644 --- a/src/app/[orgId]/settings/sites/components/SitesTable.tsx +++ b/src/app/[orgId]/settings/sites/components/SitesTable.tsx @@ -24,8 +24,8 @@ export type SiteRow = { id: number; nice: string; name: string; - mbIn: number; - mbOut: number; + mbIn: string; + mbOut: string; orgId: string; }; diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index fc758e92..6aface72 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -15,20 +15,30 @@ export default async function SitesPage(props: SitesPageProps) { try { const res = await internal.get>( `/org/${params.orgId}/sites`, - await authCookieHeader() + await authCookieHeader(), ); sites = res.data.data.sites; } catch (e) { console.error("Error fetching sites", e); } + function formatSize(mb: number): string { + if (mb >= 1024 * 1024) { + return `${(mb / (1024 * 1024)).toFixed(2)} TB`; + } else if (mb >= 1024) { + return `${(mb / 1024).toFixed(2)} GB`; + } else { + return `${mb.toFixed(2)} MB`; + } + } + const siteRows: SiteRow[] = sites.map((site) => { return { name: site.name, id: site.siteId, nice: site.niceId.toString(), - mbIn: site.megabytesIn || 0, - mbOut: site.megabytesOut || 0, + mbIn: formatSize(site.megabytesIn || 0), + mbOut: formatSize(site.megabytesOut || 0), orgId: params.orgId, }; }); diff --git a/src/app/globals.css b/src/app/globals.css index fe141f3f..9fe9acbe 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -6,11 +6,11 @@ @layer base { :root { --background: 0 0% 100%; - --foreground: 20 14.3% 4.1%; + --foreground: 20 5.0% 10.0%; --card: 0 0% 100%; - --card-foreground: 20 14.3% 4.1%; + --card-foreground: 20 5.0% 10.0%; --popover: 0 0% 100%; - --popover-foreground: 20 14.3% 4.1%; + --popover-foreground: 20 5.0% 10.0%; --primary: 24.6 95% 53.1%; --primary-foreground: 60 9.1% 97.8%; --secondary: 60 4.8% 95.9%; @@ -33,24 +33,24 @@ } .dark { - --background: 20 14.3% 4.1%; + --background: 20 5.0% 10.0%; --foreground: 60 9.1% 97.8%; - --card: 20 14.3% 4.1%; + --card: 20 5.0% 10.0%; --card-foreground: 60 9.1% 97.8%; - --popover: 20 14.3% 4.1%; + --popover: 20 5.0% 10.0%; --popover-foreground: 60 9.1% 97.8%; --primary: 20.5 90.2% 48.2%; --primary-foreground: 60 9.1% 97.8%; - --secondary: 12 6.5% 15.1%; + --secondary: 12 6.5% 25.0%; --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 15.1%; + --muted: 12 6.5% 25.0%; --muted-foreground: 24 5.4% 63.9%; - --accent: 12 6.5% 15.1%; + --accent: 12 6.5% 25.0%; --accent-foreground: 60 9.1% 97.8%; --destructive: 0 72.2% 50.6%; --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 15.1%; - --input: 12 6.5% 15.1%; + --border: 12 6.5% 25.0%; + --input: 12 6.5% 25.0%; --ring: 20.5 90.2% 48.2%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; From 8c023675074eae4825fa1d1808c121eab7fda7f8 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sun, 24 Nov 2024 22:48:17 -0500 Subject: [PATCH 03/13] show site type --- server/routers/site/listSites.ts | 32 +++++++++++-------- .../access/roles/components/RolesTable.tsx | 2 +- .../access/users/components/UsersTable.tsx | 14 ++++---- .../resources/components/ResourcesTable.tsx | 2 +- .../settings/sites/components/SitesTable.tsx | 28 ++++++++++++++-- src/app/[orgId]/settings/sites/page.tsx | 1 + 6 files changed, 54 insertions(+), 25 deletions(-) diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 83adefa2..40d84b88 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -38,14 +38,15 @@ function querySites(orgId: string, accessibleSiteIds: number[]) { megabytesIn: sites.megabytesIn, megabytesOut: sites.megabytesOut, orgName: orgs.name, + type: sites.type, }) .from(sites) .leftJoin(orgs, eq(sites.orgId, orgs.orgId)) .where( and( inArray(sites.siteId, accessibleSiteIds), - eq(sites.orgId, orgId) - ) + eq(sites.orgId, orgId), + ), ); } @@ -57,7 +58,7 @@ export type ListSitesResponse = { export async function listSites( req: Request, res: Response, - next: NextFunction + next: NextFunction, ): Promise { try { const parsedQuery = listSitesSchema.safeParse(req.query); @@ -65,8 +66,8 @@ export async function listSites( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedQuery.error) - ) + fromError(parsedQuery.error), + ), ); } const { limit, offset } = parsedQuery.data; @@ -76,8 +77,8 @@ export async function listSites( return next( createHttpError( HttpCode.BAD_REQUEST, - fromError(parsedParams.error) - ) + fromError(parsedParams.error), + ), ); } const { orgId } = parsedParams.data; @@ -86,8 +87,8 @@ export async function listSites( return next( createHttpError( HttpCode.FORBIDDEN, - "User does not have access to this organization" - ) + "User does not have access to this organization", + ), ); } @@ -100,8 +101,8 @@ export async function listSites( .where( or( eq(userSites.userId, req.user!.userId), - eq(roleSites.roleId, req.userOrgRoleId!) - ) + eq(roleSites.roleId, req.userOrgRoleId!), + ), ); const accessibleSiteIds = accessibleSites.map((site) => site.siteId); @@ -113,8 +114,8 @@ export async function listSites( .where( and( inArray(sites.siteId, accessibleSiteIds), - eq(sites.orgId, orgId) - ) + eq(sites.orgId, orgId), + ), ); const sitesList = await baseQuery.limit(limit).offset(offset); @@ -137,7 +138,10 @@ export async function listSites( }); } catch (error) { return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred", + ), ); } } diff --git a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx index 31dde81f..3b8c9c17 100644 --- a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx +++ b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx @@ -81,7 +81,7 @@ export default function UsersTable({ roles: r }: RolesTableProps) { @@ -147,7 +171,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { className="ml-2" onClick={() => router.push( - `/${siteRow.orgId}/settings/sites/${siteRow.nice}` + `/${siteRow.orgId}/settings/sites/${siteRow.nice}`, ) } > diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 6aface72..6d5455c7 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -40,6 +40,7 @@ export default async function SitesPage(props: SitesPageProps) { mbIn: formatSize(site.megabytesIn || 0), mbOut: formatSize(site.megabytesOut || 0), orgId: params.orgId, + type: site.type as any, }; }); From 93fb5e27660b4e497e33b36f1169b1c6a2b2f2dc Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sun, 24 Nov 2024 22:52:56 -0500 Subject: [PATCH 04/13] fresh resources page on update --- .../[orgId]/settings/resources/[resourceId]/general/page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index dbab044c..bff69fd3 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -34,7 +34,7 @@ import { ListSitesResponse } from "@server/routers/site"; import { useEffect, useState } from "react"; import { AxiosResponse } from "axios"; import api from "@app/api"; -import { useParams } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { GetResourceAuthInfoResponse } from "@server/routers/resource"; import { useToast } from "@app/hooks/useToast"; @@ -57,6 +57,7 @@ export default function GeneralForm() { const { toast } = useToast(); const { resource, updateResource } = useResourceContext(); const { org } = useOrgContext(); + const router = useRouter(); const orgId = params.orgId; @@ -112,6 +113,8 @@ export default function GeneralForm() { }); updateResource({ name: data.name, subdomain: data.subdomain }); + + router.refresh(); }) .finally(() => setSaveLoading(false)); } From 6abc80aa7a62fee88330ba34401239406ef8c5af Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sun, 24 Nov 2024 23:07:09 -0500 Subject: [PATCH 05/13] add border to tables and fix acesss page title spacing --- .../components/AccessPageHeaderAndNav.tsx | 17 ++++------ .../roles/components/RolesDataTable.tsx | 6 ++-- .../users/components/UsersDataTable.tsx | 2 +- .../[resourceId]/authentication/page.tsx | 4 +-- .../[resourceId]/connectivity/page.tsx | 2 +- .../components/CreateResourceForm.tsx | 32 ++++++++++--------- .../components/ResourcesDataTable.tsx | 6 ++-- .../sites/components/SitesDataTable.tsx | 2 +- src/app/[orgId]/settings/sites/page.tsx | 2 +- 9 files changed, 36 insertions(+), 37 deletions(-) diff --git a/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx b/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx index dfae7275..c6ff586c 100644 --- a/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx +++ b/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx @@ -1,5 +1,6 @@ "use client"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { SidebarSettings } from "@app/components/SidebarSettings"; type AccessPageHeaderAndNavProps = { @@ -22,16 +23,12 @@ export default function AccessPageHeaderAndNav({ return ( <> - {" "} -
-

- Users & Roles -

-

- Invite users and add them to roles to manage access to your - organization -

-
+ + {children} diff --git a/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx index 26abf881..6f004c4b 100644 --- a/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx +++ b/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx @@ -84,7 +84,7 @@ export function RolesDataTable({ Add Role
-
+
{table.getHeaderGroups().map((headerGroup) => ( @@ -97,7 +97,7 @@ export function RolesDataTable({ : flexRender( header.column.columnDef .header, - header.getContext() + header.getContext(), )} ); @@ -118,7 +118,7 @@ export function RolesDataTable({ {flexRender( cell.column.columnDef.cell, - cell.getContext() + cell.getContext(), )} ))} diff --git a/src/app/[orgId]/settings/access/users/components/UsersDataTable.tsx b/src/app/[orgId]/settings/access/users/components/UsersDataTable.tsx index f9e1e265..b70ee7c4 100644 --- a/src/app/[orgId]/settings/access/users/components/UsersDataTable.tsx +++ b/src/app/[orgId]/settings/access/users/components/UsersDataTable.tsx @@ -85,7 +85,7 @@ export function UsersDataTable({ Invite User -
+
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index e1eaa509..c7e60ce6 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -305,7 +305,7 @@ export default function ResourceAuthenticationPage() { /> )} -
+
-
+
-
+
; -const defaultValues: Partial = { - subdomain: "", - name: "My Resource", -}; - type CreateResourceFormProps = { open: boolean; setOpen: (open: boolean) => void; @@ -88,7 +83,10 @@ export default function CreateResourceForm({ const form = useForm({ resolver: zodResolver(accountFormSchema), - defaultValues, + defaultValues: { + subdomain: "", + name: "My Resource", + }, }); useEffect(() => { @@ -98,9 +96,13 @@ export default function CreateResourceForm({ const fetchSites = async () => { const res = await api.get>( - `/org/${orgId}/sites/` + `/org/${orgId}/sites/`, ); setSites(res.data.data.sites); + + if (res.data.data.sites.length > 0) { + form.setValue("siteId", res.data.data.sites[0].siteId); + } }; fetchSites(); @@ -116,7 +118,7 @@ export default function CreateResourceForm({ name: data.name, subdomain: data.subdomain, // subdomain: data.subdomain, - } + }, ) .catch((e) => { toast({ @@ -124,7 +126,7 @@ export default function CreateResourceForm({ title: "Error creating resource", description: formatAxiosError( e, - "An error occurred when creating the resource" + "An error occurred when creating the resource", ), }); }); @@ -196,7 +198,7 @@ export default function CreateResourceForm({ onChange={(value) => form.setValue( "subdomain", - value + value, ) } /> @@ -225,14 +227,14 @@ export default function CreateResourceForm({ className={cn( "w-[350px] justify-between", !field.value && - "text-muted-foreground" + "text-muted-foreground", )} > {field.value ? sites.find( (site) => site.siteId === - field.value + field.value, )?.name : "Select site"} @@ -259,7 +261,7 @@ export default function CreateResourceForm({ onSelect={() => { form.setValue( "siteId", - site.siteId + site.siteId, ); }} > @@ -269,14 +271,14 @@ export default function CreateResourceForm({ site.siteId === field.value ? "opacity-100" - : "opacity-0" + : "opacity-0", )} /> { site.name } - ) + ), )} diff --git a/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx b/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx index 2d065b24..56413dfa 100644 --- a/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx +++ b/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx @@ -85,7 +85,7 @@ export function ResourcesDataTable({ Add Resource
-
+
{table.getHeaderGroups().map((headerGroup) => ( @@ -98,7 +98,7 @@ export function ResourcesDataTable({ : flexRender( header.column.columnDef .header, - header.getContext() + header.getContext(), )} ); @@ -119,7 +119,7 @@ export function ResourcesDataTable({ {flexRender( cell.column.columnDef.cell, - cell.getContext() + cell.getContext(), )} ))} diff --git a/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx b/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx index cb4fc120..0a441fd2 100644 --- a/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx +++ b/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx @@ -85,7 +85,7 @@ export function SitesDataTable({ Add Site -
+
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 6d5455c7..63c1d7b5 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -48,7 +48,7 @@ export default async function SitesPage(props: SitesPageProps) { <> From 2312258468c81c2ff8e5e44ab4df98ab4932cfb3 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sun, 24 Nov 2024 23:28:38 -0500 Subject: [PATCH 06/13] add placeholder buttons to make all rows the same height --- .../settings/access/roles/components/RolesTable.tsx | 12 +++++++++++- .../settings/access/users/components/UsersTable.tsx | 9 ++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx index 3b8c9c17..f59e8cb9 100644 --- a/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx +++ b/src/app/[orgId]/settings/access/roles/components/RolesTable.tsx @@ -65,6 +65,14 @@ export default function UsersTable({ roles: r }: RolesTableProps) { return ( <>
+ {roleRow.isAdmin && ( + + )} {!roleRow.isAdmin && ( @@ -117,7 +125,9 @@ export default function UsersTable({ roles: r }: RolesTableProps) { roleToDelete={roleToRemove} afterDelete={() => { setRoles((prev) => - prev.filter((r) => r.roleId !== roleToRemove.roleId) + prev.filter( + (r) => r.roleId !== roleToRemove.roleId, + ), ); setUserToRemove(null); }} diff --git a/src/app/[orgId]/settings/access/users/components/UsersTable.tsx b/src/app/[orgId]/settings/access/users/components/UsersTable.tsx index f85463ec..5e1591d3 100644 --- a/src/app/[orgId]/settings/access/users/components/UsersTable.tsx +++ b/src/app/[orgId]/settings/access/users/components/UsersTable.tsx @@ -99,7 +99,9 @@ export default function UsersTable({ users: u }: UsersTableProps) { return (
- {userRow.isOwner && } + {userRow.isOwner && ( + + )} {userRow.role}
); @@ -113,6 +115,11 @@ export default function UsersTable({ users: u }: UsersTableProps) { return ( <>
+ {userRow.isOwner && ( + + )} {!userRow.isOwner && ( <> From 7c9e57ef128c7086c154f0da8215d994f6d8e455 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Mon, 25 Nov 2024 23:07:21 -0500 Subject: [PATCH 07/13] improve formatting in data tables --- server/routers/resource/listResources.ts | 2 + server/routers/user/inviteUser.ts | 35 +++++++--------- .../components/AccessPageHeaderAndNav.tsx | 2 +- .../roles/components/RolesDataTable.tsx | 32 +++++++------- .../users/components/UsersDataTable.tsx | 37 ++++++++-------- .../[orgId]/settings/components/Header.tsx | 2 +- .../[resourceId]/authentication/page.tsx | 8 ++-- .../components/ResourcesDataTable.tsx | 32 +++++++------- .../resources/components/ResourcesTable.tsx | 17 +++++++- src/app/[orgId]/settings/resources/page.tsx | 1 + .../sites/components/SitesDataTable.tsx | 36 +++++++++------- .../settings/sites/components/SitesTable.tsx | 42 +++++++++++++++++-- 12 files changed, 154 insertions(+), 92 deletions(-) diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 14b7c4b4..1bc1e896 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -58,6 +58,7 @@ function queryResources( fullDomain: resources.fullDomain, ssl: resources.ssl, siteName: sites.name, + siteId: sites.niceId, passwordId: resourcePassword.passwordId, pincodeId: resourcePincode.pincodeId, sso: resources.sso, @@ -86,6 +87,7 @@ function queryResources( ssl: resources.ssl, fullDomain: resources.fullDomain, siteName: sites.name, + siteId: sites.niceId, passwordId: resourcePassword.passwordId, sso: resources.sso, pincodeId: resourcePincode.pincodeId, diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 546ae479..84c6eb45 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -86,7 +86,6 @@ export async function inviteUser( inviteTracker[email].timestamps.push(currentTime); - logger.debug("here0") const org = await db .select() .from(orgs) @@ -98,7 +97,6 @@ export async function inviteUser( ); } - logger.debug("here1") const existingUser = await db .select() .from(users) @@ -114,7 +112,6 @@ export async function inviteUser( ); } - logger.debug("here2") const inviteId = generateRandomString( 10, alphabet("a-z", "A-Z", "0-9"), @@ -124,7 +121,6 @@ export async function inviteUser( const tokenHash = await hashPassword(token); - logger.debug("here3") // delete any existing invites for this email await db .delete(userInvites) @@ -133,7 +129,6 @@ export async function inviteUser( ) .execute(); - logger.debug("here4") await db.insert(userInvites).values({ inviteId, orgId, @@ -145,23 +140,21 @@ export async function inviteUser( const inviteLink = `${config.app.base_url}/invite?token=${inviteId}-${token}`; - logger.debug("here5") - // await sendEmail( - // SendInviteLink({ - // email, - // inviteLink, - // expiresInDays: (validHours / 24).toString(), - // orgName: org[0].name || orgId, - // inviterName: req.user?.email, - // }), - // { - // to: email, - // from: config.email?.no_reply, - // subject: "You're invited to join a Fossorial organization", - // }, - // ); + await sendEmail( + SendInviteLink({ + email, + inviteLink, + expiresInDays: (validHours / 24).toString(), + orgName: org[0].name || orgId, + inviterName: req.user?.email, + }), + { + to: email, + from: config.email?.no_reply, + subject: "You're invited to join a Fossorial organization", + }, + ); - logger.debug("here6") return response(res, { data: { inviteLink, diff --git a/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx b/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx index c6ff586c..8c1053d6 100644 --- a/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx +++ b/src/app/[orgId]/settings/access/components/AccessPageHeaderAndNav.tsx @@ -24,7 +24,7 @@ export default function AccessPageHeaderAndNav({ return ( <> diff --git a/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx index 6f004c4b..3d42edc6 100644 --- a/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx +++ b/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx @@ -22,7 +22,7 @@ import { import { Button } from "@app/components/ui/button"; import { useState } from "react"; import { Input } from "@app/components/ui/input"; -import { Plus } from "lucide-react"; +import { Plus, Search } from "lucide-react"; import { DataTablePagination } from "@app/components/DataTablePagination"; interface DataTableProps { @@ -61,19 +61,23 @@ export function RolesDataTable({ return (
- - table - .getColumn("name") - ?.setFilterValue(event.target.value) - } - className="max-w-sm mr-2" - /> +
+ + table + .getColumn("name") + ?.setFilterValue(event.target.value) + } + className="w-full pl-8" + /> + +
+ ); + }, cell: ({ row }) => { const resourceRow = row.original; return ( diff --git a/src/app/[orgId]/settings/resources/page.tsx b/src/app/[orgId]/settings/resources/page.tsx index e9aa41c9..42cf449e 100644 --- a/src/app/[orgId]/settings/resources/page.tsx +++ b/src/app/[orgId]/settings/resources/page.tsx @@ -51,6 +51,7 @@ export default async function ResourcesPage(props: ResourcesPageProps) { orgId: params.orgId, domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`, site: resource.siteName || "None", + siteId: resource.siteId || "Unknown", hasAuth: resource.sso || resource.pincodeId !== null || diff --git a/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx b/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx index 0a441fd2..e4e8af59 100644 --- a/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx +++ b/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx @@ -24,7 +24,7 @@ import { Button } from "@app/components/ui/button"; import { useState } from "react"; import { Input } from "@app/components/ui/input"; import { DataTablePagination } from "../../../../../components/DataTablePagination"; -import { Plus } from "lucide-react"; +import { Plus, Search } from "lucide-react"; interface DataTableProps { columns: ColumnDef[]; @@ -62,19 +62,23 @@ export function SitesDataTable({ return (
- - table - .getColumn("name") - ?.setFilterValue(event.target.value) - } - className="max-w-sm mr-2" - /> +
+ + table + .getColumn("name") + ?.setFilterValue(event.target.value) + } + className="w-full pl-8" + /> + +
+ ); + }, }, { accessorKey: "mbOut", - header: "MB Out", + header: ({ column }) => { + return ( + + ); + }, }, { accessorKey: "type", - header: "Connection Type", + header: ({ column }) => { + return ( + + ); + }, cell: ({ row }) => { const originalRow = row.original; From 41e531306d79ccdccd3f0572e8a15a369ee854a6 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Tue, 26 Nov 2024 20:30:52 -0500 Subject: [PATCH 08/13] protect /setup and use links for button --- server/emails/sendEmail.ts | 4 ++- .../roles/components/RolesDataTable.tsx | 2 +- .../users/components/UsersDataTable.tsx | 2 +- .../access/users/components/UsersTable.tsx | 26 ++++++++++--------- .../[orgId]/settings/components/Header.tsx | 25 +----------------- .../components/ResourcesDataTable.tsx | 2 +- .../resources/components/ResourcesTable.tsx | 17 +++++------- .../sites/components/SitesDataTable.tsx | 2 +- .../settings/sites/components/SitesTable.tsx | 17 +++++------- src/app/setup/layout.tsx | 10 +++++++ src/app/setup/page.tsx | 4 +-- 11 files changed, 48 insertions(+), 63 deletions(-) diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index f8719e0f..2a3852f2 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -1,4 +1,4 @@ -import { render } from "@react-email/components"; +import { render } from "@react-email/render"; import { ReactElement } from "react"; import emailClient from "@server/emails"; import logger from "@server/logger"; @@ -21,7 +21,9 @@ export async function sendEmail( return; } + logger.debug("Rendering email templatee...") const emailHtml = await render(template); + logger.debug("Done rendering email templatee") const options = { from: opts.from, diff --git a/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx b/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx index 3d42edc6..315d0bcc 100644 --- a/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx +++ b/src/app/[orgId]/settings/access/roles/components/RolesDataTable.tsx @@ -76,7 +76,7 @@ export function RolesDataTable({ } className="w-full pl-8" /> - +
)} @@ -161,18 +164,17 @@ export default function UsersTable({ users: u }: UsersTableProps) { )} - + + )}
diff --git a/src/app/[orgId]/settings/components/Header.tsx b/src/app/[orgId]/settings/components/Header.tsx index f4e6045e..35a7f22a 100644 --- a/src/app/[orgId]/settings/components/Header.tsx +++ b/src/app/[orgId]/settings/components/Header.tsx @@ -149,7 +149,7 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) { size="lg" role="combobox" aria-expanded={open} - className="w-full md:w-[200px] h-12 px-3 py-4 bg-neutral hover:bg-muted" + className="w-full md:w-[200px] h-12 px-3 py-4 bg-neutral hover:bg-neutral" >
@@ -202,29 +202,6 @@ export default function Header({ email, orgId, name, orgs }: HeaderProps) { - - {/* */}
diff --git a/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx b/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx index 6d04c9ad..2ffad8c1 100644 --- a/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx +++ b/src/app/[orgId]/settings/resources/components/ResourcesDataTable.tsx @@ -77,7 +77,7 @@ export function ResourcesDataTable({ } className="w-full pl-8" /> - +
+ +
); diff --git a/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx b/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx index e4e8af59..c538e3b9 100644 --- a/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx +++ b/src/app/[orgId]/settings/sites/components/SitesDataTable.tsx @@ -77,7 +77,7 @@ export function SitesDataTable({ } className="w-full pl-8" /> - +
+ + ); }, diff --git a/src/app/setup/layout.tsx b/src/app/setup/layout.tsx index 00fb0917..e0e4954f 100644 --- a/src/app/setup/layout.tsx +++ b/src/app/setup/layout.tsx @@ -1,4 +1,7 @@ +import { verifySession } from "@app/lib/auth/verifySession"; import { Metadata } from "next"; +import { redirect } from "next/navigation"; +import { cache } from "react"; export const metadata: Metadata = { title: `Setup - Pangolin`, @@ -10,5 +13,12 @@ export default async function SetupLayout({ }: { children: React.ReactNode; }) { + const getUser = cache(verifySession); + const user = await getUser(); + + if (!user) { + redirect("/"); + } + return
{children}
; } diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index fe423f9a..95b71e01 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -44,7 +44,7 @@ export default function StepperForm() { const debouncedCheckOrgIdAvailability = useCallback( debounce(checkOrgIdAvailability, 300), - [checkOrgIdAvailability] + [checkOrgIdAvailability], ); useEffect(() => { @@ -278,7 +278,7 @@ export default function StepperForm() { function debounce any>( func: T, - wait: number + wait: number, ): (...args: Parameters) => void { let timeout: NodeJS.Timeout | null = null; From 8178dd15253433eeded32066c5becf634b8be530 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Wed, 27 Nov 2024 00:07:40 -0500 Subject: [PATCH 09/13] set resource session as base domain cookie --- server/auth/resource.ts | 20 +++--- server/config.ts | 4 ++ server/routers/auth/checkResourceSession.ts | 65 +++++++++++++++++++ server/routers/auth/index.ts | 1 + server/routers/badger/verifySession.ts | 28 +++++--- server/routers/internal.ts | 5 ++ server/routers/resource/authWithPassword.ts | 17 ++--- server/routers/resource/authWithPincode.ts | 17 ++--- src/api/index.ts | 8 +++ .../resources/components/ResourcesTable.tsx | 2 +- .../components/ResourceAuthPortal.tsx | 17 ++--- src/app/auth/resource/[resourceId]/page.tsx | 35 ++++++++-- src/app/setup/layout.tsx | 2 + 13 files changed, 169 insertions(+), 52 deletions(-) create mode 100644 server/routers/auth/checkResourceSession.ts diff --git a/server/auth/resource.ts b/server/auth/resource.ts index f9697a70..b13b54d9 100644 --- a/server/auth/resource.ts +++ b/server/auth/resource.ts @@ -3,9 +3,13 @@ import { sha256 } from "@oslojs/crypto/sha2"; import { resourceSessions, ResourceSession } from "@server/db/schema"; import db from "@server/db"; import { eq, and } from "drizzle-orm"; +import config from "@server/config"; export const SESSION_COOKIE_NAME = "resource_session"; export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30; +export const SECURE_COOKIES = config.server.secure_cookies; +export const COOKIE_DOMAIN = + "." + new URL(config.app.base_url).hostname.split(".").slice(-2).join("."); export async function createResourceSession(opts: { token: string; @@ -115,25 +119,25 @@ export async function invalidateAllSessions( } export function serializeResourceSessionCookie( + cookieName: string, token: string, fqdn: string, - secure: boolean, ): string { - if (secure) { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=.localhost`; + if (SECURE_COOKIES) { + return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { - return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=.localhost`; + return `${cookieName}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`; } } export function createBlankResourceSessionTokenCookie( + cookieName: string, fqdn: string, - secure: boolean, ): string { - if (secure) { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${fqdn}`; + if (SECURE_COOKIES) { + return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`; } else { - return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${fqdn}`; + return `${cookieName}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`; } } diff --git a/server/config.ts b/server/config.ts index 62bdfab4..1314208a 100644 --- a/server/config.ts +++ b/server/config.ts @@ -128,11 +128,15 @@ if (!parsedConfig.success) { process.env.SERVER_EXTERNAL_PORT = parsedConfig.data.server.external_port.toString(); +process.env.SERVER_INTERNAL_PORT = + parsedConfig.data.server.internal_port.toString(); process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags ?.require_email_verification ? "true" : "false"; process.env.SESSION_COOKIE_NAME = parsedConfig.data.server.session_cookie_name; +process.env.RESOURCE_SESSION_COOKIE_NAME = + parsedConfig.data.badger.resource_session_cookie_name; process.env.RESOURCE_SESSION_QUERY_PARAM_NAME = parsedConfig.data.badger.session_query_parameter; diff --git a/server/routers/auth/checkResourceSession.ts b/server/routers/auth/checkResourceSession.ts new file mode 100644 index 00000000..11f4ca90 --- /dev/null +++ b/server/routers/auth/checkResourceSession.ts @@ -0,0 +1,65 @@ +import { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import HttpCode from "@server/types/HttpCode"; +import { response } from "@server/utils"; +import { validateSessionToken } from "@server/auth"; +import { validateResourceSessionToken } from "@server/auth/resource"; + +export const params = z.object({ + token: z.string(), + resourceId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export type CheckResourceSessionParams = z.infer; + +export type CheckResourceSessionResponse = { + valid: boolean; +}; + +export async function checkResourceSession( + req: Request, + res: Response, + next: NextFunction, +): Promise { + const parsedParams = params.safeParse(req.params); + + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString(), + ), + ); + } + + const { token, resourceId } = parsedParams.data; + + try { + const { resourceSession } = await validateResourceSessionToken( + token, + resourceId, + ); + + let valid = false; + if (resourceSession) { + valid = true; + } + + return response(res, { + data: { valid }, + success: true, + error: false, + message: "Checked validity", + status: HttpCode.OK, + }); + } catch (e) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to reset password", + ), + ); + } +} diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index b6ce3a01..b2eaf8d2 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -9,3 +9,4 @@ export * from "./requestEmailVerificationCode"; export * from "./changePassword"; export * from "./requestPasswordReset"; export * from "./resetPassword"; +export * from "./checkResourceSession"; diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index cd8529a1..042713bf 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -19,10 +19,7 @@ import { Resource, roleResources, userResources } from "@server/db/schema"; import logger from "@server/logger"; const verifyResourceSessionSchema = z.object({ - sessions: z.object({ - session: z.string().nullable(), - resource_session: z.string().nullable(), - }), + sessions: z.record(z.string()).optional(), originalRequestURL: z.string().url(), scheme: z.string(), host: z.string(), @@ -98,10 +95,15 @@ export async function verifyResourceSession( const redirectUrl = `${config.app.base_url}/auth/resource/${encodeURIComponent(resource.resourceId)}?redirect=${encodeURIComponent(originalRequestURL)}`; - if (sso && sessions.session) { - const { session, user } = await validateSessionToken( - sessions.session, - ); + if (!sessions) { + return notAllowed(res); + } + + const sessionToken = sessions[config.server.session_cookie_name]; + + // check for unified login + if (sso && sessionToken) { + const { session, user } = await validateSessionToken(sessionToken); if (session && user) { const isAllowed = await isUserAllowedToAccessResource( user.userId, @@ -117,11 +119,17 @@ export async function verifyResourceSession( } } - if (password && sessions.resource_session) { + const resourceSessionToken = + sessions[ + `${config.badger.resource_session_cookie_name}_${resource.resourceId}` + ]; + + if ((pincode || password) && resourceSessionToken) { const { resourceSession } = await validateResourceSessionToken( - sessions.resource_session, + resourceSessionToken, resource.resourceId, ); + if (resourceSession) { if ( pincode && diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 42aa7d47..fb0e1809 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -2,6 +2,7 @@ import { Router } from "express"; import * as gerbil from "@server/routers/gerbil"; import * as badger from "@server/routers/badger"; import * as traefik from "@server/routers/traefik"; +import * as auth from "@server/routers/auth"; import HttpCode from "@server/types/HttpCode"; // Root routes @@ -12,6 +13,10 @@ internalRouter.get("/", (_, res) => { }); internalRouter.get("/traefik-config", traefik.traefikConfigProvider); +internalRouter.get( + "/resource-session/:resourceId/:token", + auth.checkResourceSession, +); // Gerbil routes const gerbilRouter = Router(); diff --git a/server/routers/resource/authWithPassword.ts b/server/routers/resource/authWithPassword.ts index d803f279..b2a42572 100644 --- a/server/routers/resource/authWithPassword.ts +++ b/server/routers/resource/authWithPassword.ts @@ -14,6 +14,7 @@ import { serializeResourceSessionCookie, } from "@server/auth/resource"; import logger from "@server/logger"; +import config from "@server/config"; export const authWithPasswordBodySchema = z.object({ password: z.string(), @@ -131,15 +132,15 @@ export async function authWithPassword( token, passwordId: definedPassword.passwordId, }); - // const secureCookie = resource.ssl; - // const cookie = serializeResourceSessionCookie( - // token, - // resource.fullDomain, - // secureCookie, - // ); - // res.appendHeader("Set-Cookie", cookie); + const cookieName = `${config.badger.resource_session_cookie_name}_${resource.resourceId}`; + const cookie = serializeResourceSessionCookie( + cookieName, + token, + resource.fullDomain, + ); + res.appendHeader("Set-Cookie", cookie); - // logger.debug(cookie); // remove after testing + logger.debug(cookie); // remove after testing return response(res, { data: { diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts index 8c618f8a..a89b1584 100644 --- a/server/routers/resource/authWithPincode.ts +++ b/server/routers/resource/authWithPincode.ts @@ -14,6 +14,7 @@ import { serializeResourceSessionCookie, } from "@server/auth/resource"; import logger from "@server/logger"; +import config from "@server/config"; export const authWithPincodeBodySchema = z.object({ pincode: z.string(), @@ -127,15 +128,15 @@ export async function authWithPincode( token, pincodeId: definedPincode.pincodeId, }); - // const secureCookie = resource.ssl; - // const cookie = serializeResourceSessionCookie( - // token, - // resource.fullDomain, - // secureCookie, - // ); - // res.appendHeader("Set-Cookie", cookie); + const cookieName = `${config.badger.resource_session_cookie_name}_${resource.resourceId}`; + const cookie = serializeResourceSessionCookie( + cookieName, + token, + resource.fullDomain, + ); + res.appendHeader("Set-Cookie", cookie); - // logger.debug(cookie); // remove after testing + logger.debug(cookie); // remove after testing return response(res, { data: { diff --git a/src/api/index.ts b/src/api/index.ts index 4da1a36e..d0c687f1 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -22,4 +22,12 @@ export const internal = axios.create({ }, }); +export const priv = axios.create({ + baseURL: `http://localhost:${process.env.SERVER_INTERNAL_PORT}/api/v1`, + timeout: 10000, + headers: { + "Content-Type": "application/json", + }, +}); + export default api; diff --git a/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx index 137b398a..e8dfc643 100644 --- a/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/components/ResourcesTable.tsx @@ -124,7 +124,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { return (
{ const session = res.data.data.session; if (session) { - window.location.href = constructRedirect( - props.redirect, - session, - ); + const url = constructRedirect(props.redirect); + console.log(url); + window.location.href = url; } }) .catch((e) => { @@ -156,10 +152,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { .then((res) => { const session = res.data.data.session; if (session) { - window.location.href = constructRedirect( - props.redirect, - session, - ); + window.location.href = constructRedirect(props.redirect); } }) .catch((e) => { diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 32b7d067..6327970f 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -3,7 +3,7 @@ import { GetResourceResponse, } from "@server/routers/resource"; import ResourceAuthPortal from "./components/ResourceAuthPortal"; -import { internal } from "@app/api"; +import { internal, priv } from "@app/api"; import { AxiosResponse } from "axios"; import { authCookieHeader } from "@app/api/cookies"; import { cache } from "react"; @@ -11,10 +11,12 @@ import { verifySession } from "@app/lib/auth/verifySession"; import { redirect } from "next/navigation"; import ResourceNotFound from "./components/ResourceNotFound"; import ResourceAccessDenied from "./components/ResourceAccessDenied"; +import { cookies } from "next/headers"; +import { CheckResourceSessionResponse } from "@server/routers/auth"; export default async function ResourceAuthPage(props: { params: Promise<{ resourceId: number }>; - searchParams: Promise<{ redirect: string }>; + searchParams: Promise<{ redirect: string | undefined }>; }) { const params = await props.params; const searchParams = await props.searchParams; @@ -46,6 +48,32 @@ export default async function ResourceAuthPage(props: { const redirectUrl = searchParams.redirect || authInfo.url; + const allCookies = await cookies(); + const cookieName = + process.env.RESOURCE_SESSION_COOKIE_NAME + `_${params.resourceId}`; + const sessionId = allCookies.get(cookieName)?.value ?? null; + + if (sessionId) { + let doRedirect = false; + try { + const res = await priv.get< + AxiosResponse + >(`/resource-session/${params.resourceId}/${sessionId}`); + + console.log("resource session already exists and is valid"); + + if (res && res.data.data.valid) { + doRedirect = true; + } + } catch (e) { + console.error(e); + } + + if (doRedirect) { + redirect(redirectUrl); + } + } + if (!hasAuth) { // no authentication so always go straight to the resource redirect(redirectUrl); @@ -94,9 +122,6 @@ export default async function ResourceAuthPage(props: { id: authInfo.resourceId, }} redirect={redirectUrl} - queryParamName={ - process.env.RESOURCE_SESSION_QUERY_PARAM_NAME! - } />
diff --git a/src/app/setup/layout.tsx b/src/app/setup/layout.tsx index e0e4954f..c8c96ac7 100644 --- a/src/app/setup/layout.tsx +++ b/src/app/setup/layout.tsx @@ -8,6 +8,8 @@ export const metadata: Metadata = { description: "", }; +export const dynamic = "force-dynamic"; + export default async function SetupLayout({ children, }: { From c2cbd7e1a147795d14ff7050c1f932bd15135387 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Wed, 27 Nov 2024 14:35:38 -0500 Subject: [PATCH 10/13] fix minor auth issues and set NODE_ENV to solve react email bug --- config.example.yml | 13 ++-- package.json | 2 +- server/config.ts | 7 +- server/routers/badger/verifySession.ts | 2 +- server/routers/resource/authWithPassword.ts | 2 +- server/routers/resource/authWithPincode.ts | 2 +- server/routers/traefik/getTraefikConfig.ts | 4 +- .../[resourceId]/connectivity/page.tsx | 76 +++++++++---------- .../components/ResourceAuthPortal.tsx | 10 +-- src/app/auth/resource/[resourceId]/page.tsx | 47 +++++------- 10 files changed, 75 insertions(+), 90 deletions(-) diff --git a/config.example.yml b/config.example.yml index 0c8ed323..fe73e4d0 100644 --- a/config.example.yml +++ b/config.example.yml @@ -1,14 +1,15 @@ app: - base_url: http://localhost:3000 + base_url: https://fossorial.io log_level: debug save_logs: false server: external_port: 3000 internal_port: 3001 - internal_hostname: localhost + internal_hostname: pangolin secure_cookies: false session_cookie_name: session + resource_session_cookie_name: resource_session traefik: cert_resolver: letsencrypt @@ -16,16 +17,12 @@ traefik: https_entrypoint: websecure prefer_wildcard_cert: true -badger: - session_query_parameter: __pang_sess - resource_session_cookie_name: resource_session - gerbil: start_port: 51820 - base_endpoint: localhost + base_endpoint: fossorial.io + use_subdomain: false block_size: 16 subnet_group: 10.0.0.0/8 - use_subdomain: true rate_limit: window_minutes: 1 diff --git a/package.json b/package.json index 26a7d454..45a1ad21 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "db:hydrate": "npx tsx scripts/hydrate.ts", "db:studio": "drizzle-kit studio", "build": "mkdir -p dist && next build && node scripts/esbuild.mjs -e server/index.ts -o dist/server.mjs", - "start": "ENVIRONMENT=prod node dist/server.mjs", + "start": "NODE_ENV=development ENVIRONMENT=prod node dist/server.mjs", "email": "email dev --dir server/emails/templates --port 3002" }, "dependencies": { diff --git a/server/config.ts b/server/config.ts index 1314208a..ee1cc3ea 100644 --- a/server/config.ts +++ b/server/config.ts @@ -25,9 +25,6 @@ const environmentSchema = z.object({ secure_cookies: z.boolean(), signup_secret: z.string().optional(), session_cookie_name: z.string(), - }), - badger: z.object({ - session_query_parameter: z.string(), resource_session_cookie_name: z.string(), }), traefik: z.object({ @@ -136,8 +133,6 @@ process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags : "false"; process.env.SESSION_COOKIE_NAME = parsedConfig.data.server.session_cookie_name; process.env.RESOURCE_SESSION_COOKIE_NAME = - parsedConfig.data.badger.resource_session_cookie_name; -process.env.RESOURCE_SESSION_QUERY_PARAM_NAME = - parsedConfig.data.badger.session_query_parameter; + parsedConfig.data.server.resource_session_cookie_name; export default parsedConfig.data; diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 042713bf..25b212fa 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -121,7 +121,7 @@ export async function verifyResourceSession( const resourceSessionToken = sessions[ - `${config.badger.resource_session_cookie_name}_${resource.resourceId}` + `${config.server.resource_session_cookie_name}_${resource.resourceId}` ]; if ((pincode || password) && resourceSessionToken) { diff --git a/server/routers/resource/authWithPassword.ts b/server/routers/resource/authWithPassword.ts index b2a42572..4e1ae35e 100644 --- a/server/routers/resource/authWithPassword.ts +++ b/server/routers/resource/authWithPassword.ts @@ -132,7 +132,7 @@ export async function authWithPassword( token, passwordId: definedPassword.passwordId, }); - const cookieName = `${config.badger.resource_session_cookie_name}_${resource.resourceId}`; + const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; const cookie = serializeResourceSessionCookie( cookieName, token, diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts index a89b1584..8d69ede1 100644 --- a/server/routers/resource/authWithPincode.ts +++ b/server/routers/resource/authWithPincode.ts @@ -128,7 +128,7 @@ export async function authWithPincode( token, pincodeId: definedPincode.pincodeId, }); - const cookieName = `${config.badger.resource_session_cookie_name}_${resource.resourceId}`; + const cookieName = `${config.server.resource_session_cookie_name}_${resource.resourceId}`; const cookie = serializeResourceSessionCookie( cookieName, token, diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index dc2ecb56..b73bc8f7 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -55,11 +55,9 @@ export async function traefikConfigProvider( `http://${config.server.internal_hostname}:${config.server.internal_port}`, ).href, resourceSessionCookieName: - config.badger.resource_session_cookie_name, + config.server.resource_session_cookie_name, userSessionCookieName: config.server.session_cookie_name, - sessionQueryParameter: - config.badger.session_query_parameter, }, }, }, diff --git a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx index 1d8aa2eb..a9df5ae1 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/connectivity/page.tsx @@ -233,6 +233,24 @@ export default function ReverseProxyTargets(props: { } const columns: ColumnDef[] = [ + { + accessorKey: "method", + header: "Method", + cell: ({ row }) => ( + + ), + }, { accessorKey: "ip", header: "IP Address", @@ -262,24 +280,6 @@ export default function ReverseProxyTargets(props: { /> ), }, - { - accessorKey: "method", - header: "Method", - cell: ({ row }) => ( - - ), - }, // { // accessorKey: "protocol", // header: "Protocol", @@ -368,7 +368,7 @@ export default function ReverseProxyTargets(props: { -
+
- ( - - - IP Address - - - - - - Enter the IP address of the - target. - - - - )} - /> )} /> + ( + + + IP Address + + + + + + Enter the IP address of the + target. + + + + )} + /> { + try { + await api.get(`/resource/${props.resource.id}`); + } catch (e) { setAccessDenied(true); - }); + } if (!accessDenied) { - window.location.href = props.redirect; + window.location.href = constructRedirect(props.redirect); } } diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 6327970f..1c436171 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -65,9 +65,7 @@ export default async function ResourceAuthPage(props: { if (res && res.data.data.valid) { doRedirect = true; } - } catch (e) { - console.error(e); - } + } catch (e) {} if (doRedirect) { redirect(redirectUrl); @@ -91,7 +89,6 @@ export default async function ResourceAuthPage(props: { console.log(res.data); doRedirect = true; } catch (e) { - console.error(e); userIsUnauthorized = true; } @@ -100,30 +97,28 @@ export default async function ResourceAuthPage(props: { } } - if (userIsUnauthorized && isSSOOnly) { - return ( -
- -
- ); - } - return ( <> -
- -
+ {userIsUnauthorized && isSSOOnly ? ( +
+ +
+ ) : ( +
+ +
+ )} ); } From 5bbf32f6a68e535064642786942742976a3b87ec Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Thu, 28 Nov 2024 00:11:13 -0500 Subject: [PATCH 11/13] improve verify email redirect flow --- server/emails/sendEmail.ts | 2 -- server/emails/templates/SendInviteLink.tsx | 14 +++++++-- server/emails/templates/VerifyEmailCode.tsx | 29 +++++++++++++++---- server/routers/auth/login.ts | 2 +- .../routers/auth/sendEmailVerificationCode.ts | 17 +++++++---- src/app/[orgId]/layout.tsx | 21 ++++++++++++++ src/app/[orgId]/page.tsx | 21 -------------- src/app/[orgId]/settings/general/layout.tsx | 10 +++---- src/app/[orgId]/settings/layout.tsx | 8 ++--- src/app/auth/login/DashboardLoginForm.tsx | 10 ++++++- .../components/ResourceAuthPortal.tsx | 15 ++++------ src/app/auth/verify-email/VerifyEmailForm.tsx | 5 ++-- src/app/auth/verify-email/page.tsx | 4 +-- src/app/invite/page.tsx | 2 +- src/app/page.tsx | 23 ++++++++++++--- src/app/setup/layout.tsx | 2 +- src/components/LoginForm.tsx | 15 ++++------ src/lib/auth/verifySession.ts | 28 ++++++++++++++---- 18 files changed, 145 insertions(+), 83 deletions(-) diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index 2a3852f2..973f48ee 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -21,9 +21,7 @@ export async function sendEmail( return; } - logger.debug("Rendering email templatee...") const emailHtml = await render(template); - logger.debug("Done rendering email templatee") const options = { from: opts.from, diff --git a/server/emails/templates/SendInviteLink.tsx b/server/emails/templates/SendInviteLink.tsx index cc05881d..fabcffbd 100644 --- a/server/emails/templates/SendInviteLink.tsx +++ b/server/emails/templates/SendInviteLink.tsx @@ -33,9 +33,17 @@ export const SendInviteLink = ({ {previewText} - + - + You're invited to join a Fossorial organization @@ -58,7 +66,7 @@ export const SendInviteLink = ({
diff --git a/server/emails/templates/VerifyEmailCode.tsx b/server/emails/templates/VerifyEmailCode.tsx index dcae2971..fc8978ed 100644 --- a/server/emails/templates/VerifyEmailCode.tsx +++ b/server/emails/templates/VerifyEmailCode.tsx @@ -14,11 +14,13 @@ import * as React from "react"; interface VerifyEmailProps { username?: string; verificationCode: string; + verifyLink: string; } export const VerifyEmail = ({ username, verificationCode, + verifyLink, }: VerifyEmailProps) => { const previewText = `Verify your email, ${username}`; @@ -26,21 +28,34 @@ export const VerifyEmail = ({ {previewText} - + - + - Verify Your Email + Please verify your email Hi {username || "there"}, - You’ve requested to verify your email. Please use - the verification code below: + You’ve requested to verify your email. Please{" "} + + click here + {" "} + to verify your email, then enter the following code:
- + {verificationCode}
@@ -59,3 +74,5 @@ export const VerifyEmail = ({ ); }; + +export default VerifyEmail; diff --git a/server/routers/auth/login.ts b/server/routers/auth/login.ts index 9ffe651c..b7b68e47 100644 --- a/server/routers/auth/login.ts +++ b/server/routers/auth/login.ts @@ -139,7 +139,7 @@ export async function login( success: true, error: false, message: "Email verification code sent", - status: HttpCode.ACCEPTED, + status: HttpCode.OK, }); } diff --git a/server/routers/auth/sendEmailVerificationCode.ts b/server/routers/auth/sendEmailVerificationCode.ts index 3a167405..334eb23a 100644 --- a/server/routers/auth/sendEmailVerificationCode.ts +++ b/server/routers/auth/sendEmailVerificationCode.ts @@ -13,11 +13,18 @@ export async function sendEmailVerificationCode( ): Promise { const code = await generateEmailVerificationCode(userId, email); - await sendEmail(VerifyEmail({ username: email, verificationCode: code }), { - to: email, - from: config.email?.no_reply, - subject: "Verify your email address", - }); + await sendEmail( + VerifyEmail({ + username: email, + verificationCode: code, + verifyLink: `${config.app.base_url}/auth/verify-email`, + }), + { + to: email, + from: config.email?.no_reply, + subject: "Verify your email address", + }, + ); } async function generateEmailVerificationCode( diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx index e02e8af2..cc0d59be 100644 --- a/src/app/[orgId]/layout.tsx +++ b/src/app/[orgId]/layout.tsx @@ -1,6 +1,8 @@ import { internal } from "@app/api"; import { authCookieHeader } from "@app/api/cookies"; +import { verifySession } from "@app/lib/auth/verifySession"; import { GetOrgResponse } from "@server/routers/org"; +import { GetOrgUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { cache } from "react"; @@ -17,6 +19,25 @@ export default async function OrgLayout(props: { redirect(`/`); } + const getUser = cache(verifySession); + const user = await getUser(); + + if (!user) { + redirect(`/?redirect=/${orgId}`); + } + + try { + const getOrgUser = cache(() => + internal.get>( + `/org/${orgId}/user/${user.userId}`, + cookie, + ), + ); + const orgUser = await getOrgUser(); + } catch { + redirect(`/`); + } + try { const getOrg = cache(() => internal.get>( diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx index 730e6851..cb3297d3 100644 --- a/src/app/[orgId]/page.tsx +++ b/src/app/[orgId]/page.tsx @@ -14,27 +14,6 @@ export default async function OrgPage(props: OrgPageProps) { const params = await props.params; const orgId = params.orgId; - const getUser = cache(verifySession); - const user = await getUser(); - - if (!user) { - redirect("/auth/login"); - } - - const cookie = await authCookieHeader(); - - try { - const getOrgUser = cache(() => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - cookie - ) - ); - const orgUser = await getOrgUser(); - } catch { - redirect(`/`); - } - return ( <>

Welcome to {orgId} dashboard

diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index ee413b9e..f4dc6549 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -26,7 +26,7 @@ export default async function GeneralSettingsPage({ const user = await getUser(); if (!user) { - redirect("/auth/login"); + redirect(`/?redirect=/${orgId}/settings/general`); } let orgUser = null; @@ -34,8 +34,8 @@ export default async function GeneralSettingsPage({ const getOrgUser = cache(async () => internal.get>( `/org/${orgId}/user/${user.userId}`, - await authCookieHeader() - ) + await authCookieHeader(), + ), ); const res = await getOrgUser(); orgUser = res.data.data; @@ -48,8 +48,8 @@ export default async function GeneralSettingsPage({ const getOrg = cache(async () => internal.get>( `/org/${orgId}`, - await authCookieHeader() - ) + await authCookieHeader(), + ), ); const res = await getOrg(); org = res.data.data; diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index 45095896..b277b7f3 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -55,7 +55,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const user = await getUser(); if (!user) { - redirect("/auth/login"); + redirect(`/?redirect=/${params.orgId}/`); } const cookie = await authCookieHeader(); @@ -64,8 +64,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const getOrgUser = cache(() => internal.get>( `/org/${params.orgId}/user/${user.userId}`, - cookie - ) + cookie, + ), ); const orgUser = await getOrgUser(); @@ -79,7 +79,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { let orgs: ListOrgsResponse["orgs"] = []; try { const getOrgs = cache(() => - internal.get>(`/orgs`, cookie) + internal.get>(`/orgs`, cookie), ); const res = await getOrgs(); if (res && res.data.data.orgs) { diff --git a/src/app/auth/login/DashboardLoginForm.tsx b/src/app/auth/login/DashboardLoginForm.tsx index 986993cd..61abe8cc 100644 --- a/src/app/auth/login/DashboardLoginForm.tsx +++ b/src/app/auth/login/DashboardLoginForm.tsx @@ -30,7 +30,15 @@ export default function DashboardLoginForm({ router.push("/")} + onLogin={() => { + if (redirect && redirect.includes("http")) { + window.location.href = redirect; + } else if (redirect) { + router.push(redirect); + } else { + router.push("/"); + } + }} /> diff --git a/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx index 3556bf51..4e3a612b 100644 --- a/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx @@ -38,6 +38,7 @@ import { LoginResponse } from "@server/routers/auth"; import ResourceAccessDenied from "./ResourceAccessDenied"; import LoginForm from "@app/components/LoginForm"; import { AuthWithPasswordResponse } from "@server/routers/resource"; +import { redirect } from "next/dist/server/api-utils"; const pinSchema = z.object({ pin: z @@ -113,11 +114,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { }, }); - function constructRedirect(redirect: string): string { - const redirectUrl = new URL(redirect); - return redirectUrl.toString(); - } - const onPinSubmit = (values: z.infer) => { setLoadingLogin(true); api.post>( @@ -127,9 +123,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { .then((res) => { const session = res.data.data.session; if (session) { - const url = constructRedirect(props.redirect); - console.log(url); - window.location.href = url; + window.location.href = props.redirect; } }) .catch((e) => { @@ -152,7 +146,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { .then((res) => { const session = res.data.data.session; if (session) { - window.location.href = constructRedirect(props.redirect); + window.location.href = props.redirect; } }) .catch((e) => { @@ -172,7 +166,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { } if (!accessDenied) { - window.location.href = constructRedirect(props.redirect); + window.location.href = props.redirect; } } @@ -371,6 +365,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { className={`${numMethods <= 1 ? "mt-0" : ""}`} > await handleSSOAuth() } diff --git a/src/app/auth/verify-email/VerifyEmailForm.tsx b/src/app/auth/verify-email/VerifyEmailForm.tsx index a8cedf9c..cceddc66 100644 --- a/src/app/auth/verify-email/VerifyEmailForm.tsx +++ b/src/app/auth/verify-email/VerifyEmailForm.tsx @@ -79,6 +79,7 @@ export default function VerifyEmailForm({ .catch((e) => { setError(formatAxiosError(e, "An error occurred")); console.error("Failed to verify email:", e); + setIsSubmitting(false); }); if (res && res.data?.data?.valid) { @@ -125,7 +126,7 @@ export default function VerifyEmailForm({
- Verify Your Email + Verify Email Enter the verification code sent to your email address. @@ -234,7 +235,7 @@ export default function VerifyEmailForm({ -
+
+
); diff --git a/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx index 4e3a612b..014c8503 100644 --- a/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; @@ -23,7 +23,7 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; -import { LockIcon, UserIcon, Binary, Key, User } from "lucide-react"; +import { LockIcon, Binary, Key, User } from "lucide-react"; import { InputOTP, InputOTPGroup, @@ -34,11 +34,10 @@ import { useRouter } from "next/navigation"; import { Alert, AlertDescription } from "@app/components/ui/alert"; import { formatAxiosError } from "@app/lib/utils"; import { AxiosResponse } from "axios"; -import { LoginResponse } from "@server/routers/auth"; -import ResourceAccessDenied from "./ResourceAccessDenied"; import LoginForm from "@app/components/LoginForm"; import { AuthWithPasswordResponse } from "@server/routers/resource"; import { redirect } from "next/dist/server/api-utils"; +import ResourceAccessDenied from "./ResourceAccessDenied"; const pinSchema = z.object({ pin: z @@ -159,13 +158,15 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { }; async function handleSSOAuth() { + let isAllowed = false; try { await api.get(`/resource/${props.resource.id}`); + isAllowed = true; } catch (e) { setAccessDenied(true); } - if (!accessDenied) { + if (isAllowed) { window.location.href = props.redirect; } } @@ -174,6 +175,11 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
{!accessDenied ? (
+
+ + Powered by Fossorial + +
Authentication Required @@ -365,7 +371,11 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { className={`${numMethods <= 1 ? "mt-0" : ""}`} > await handleSSOAuth() } diff --git a/src/app/auth/resource/[resourceId]/components/ResourceNotFound.tsx b/src/app/auth/resource/[resourceId]/components/ResourceNotFound.tsx index d72f5633..5b101297 100644 --- a/src/app/auth/resource/[resourceId]/components/ResourceNotFound.tsx +++ b/src/app/auth/resource/[resourceId]/components/ResourceNotFound.tsx @@ -1,3 +1,4 @@ +import { Button } from "@app/components/ui/button"; import { Card, CardContent, @@ -5,6 +6,7 @@ import { CardHeader, CardTitle, } from "@app/components/ui/card"; +import Link from "next/link"; export default async function ResourceNotFound() { return ( @@ -15,7 +17,12 @@ export default async function ResourceNotFound() { - The resource you're trying to access does not exist + The resource you're trying to access does not exist. +
+ +
); diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 1c436171..23b27c43 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -33,7 +33,7 @@ export default async function ResourceAuthPage(props: { } catch (e) {} const getUser = cache(verifySession); - const user = await getUser(); + const user = await getUser({ skipCheckVerifyEmail: true }); if (!authInfo) { return ( @@ -48,6 +48,16 @@ export default async function ResourceAuthPage(props: { const redirectUrl = searchParams.redirect || authInfo.url; + if ( + user && + !user.emailVerified && + process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED === "true" + ) { + redirect( + `/auth/verify-email?redirect=/auth/resource/${authInfo.resourceId}`, + ); + } + const allCookies = await cookies(); const cookieName = process.env.RESOURCE_SESSION_COOKIE_NAME + `_${params.resourceId}`; diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index c57d9ab9..b7a04c2f 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -72,14 +72,11 @@ export default function LoginForm({ redirect, onLogin }: LoginFormProps) { ); }); - console.log(res); - if (res && res.status === 200) { setError(null); if (res.data?.data?.emailVerificationRequired) { if (redirect) { - console.log("here", redirect) router.push(`/auth/verify-email?redirect=${redirect}`); } else { router.push("/auth/verify-email"); From f2457e77321aeb45fb5874393314f9f9e3dd1d02 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sat, 30 Nov 2024 23:51:22 -0500 Subject: [PATCH 13/13] otp schema --- server/db/schema.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/server/db/schema.ts b/server/db/schema.ts index 2c462bb5..26ea4b0c 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -287,16 +287,28 @@ export const resourceSessions = sqliteTable("resourceSessions", { () => resourcePassword.passwordId, { onDelete: "cascade", - } + }, ), pincodeId: integer("pincodeId").references( () => resourcePincode.pincodeId, { onDelete: "cascade", - } + }, ), }); +export const resourceOtp = sqliteTable("resourceOtp", { + otpId: integer("otpId").primaryKey({ + autoIncrement: true, + }), + resourceId: integer("resourceId") + .notNull() + .references(() => resources.resourceId, { onDelete: "cascade" }), + email: text("email").notNull(), + otpHash: text("otpHash").notNull(), + expiresAt: integer("expiresAt").notNull(), +}); + export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; @@ -325,3 +337,4 @@ export type UserOrg = InferSelectModel; export type ResourceSession = InferSelectModel; export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; +export type ResourceOtp = InferSelectModel;