From 3c9f0946d2df6db8199fb654fd1b2c50a1839464 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 16 Jul 2026 17:56:31 -0400 Subject: [PATCH 01/19] add site provisioning keys to integration api --- messages/en-US.json | 4 + server/middlewares/integration/index.ts | 1 + .../verifyApiKeySiteProvisioningKeyAccess.ts | 111 ++++++++++++++++++ server/openApi.ts | 1 + server/private/routers/integration.ts | 44 +++++++ .../createSiteProvisioningKey.ts | 60 +++++++++- .../deleteSiteProvisioningKey.ts | 33 +++++- .../listSiteProvisioningKeys.ts | 56 +++++++-- .../updateSiteProvisioningKey.ts | 61 ++++++++-- src/components/PermissionsSelectBox.tsx | 7 ++ 10 files changed, 355 insertions(+), 23 deletions(-) create mode 100644 server/middlewares/integration/verifyApiKeySiteProvisioningKeyAccess.ts diff --git a/messages/en-US.json b/messages/en-US.json index 6893b41fa..678c73ebd 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "List Invitations", "actionExportLogs": "Export Logs", "actionViewLogs": "View Logs", + "actionCreateSiteProvisioningKey": "Create Site Provisioning Key", + "actionListSiteProvisioningKeys": "List Site Provisioning Keys", + "actionUpdateSiteProvisioningKey": "Update Site Provisioning Key", + "actionDeleteSiteProvisioningKey": "Delete Site Provisioning Key", "noneSelected": "None selected", "orgNotFound2": "No organizations found.", "search": "Search…", diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index d43854eab..be9d400cf 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -17,3 +17,4 @@ export * from "./verifyApiKeySiteResourceAccess"; export * from "./verifyApiKeyIdpAccess"; export * from "./verifyApiKeyDomainAccess"; export * from "./verifyApiKeyResourcePolicyAccess"; +export * from "./verifyApiKeySiteProvisioningKeyAccess"; diff --git a/server/middlewares/integration/verifyApiKeySiteProvisioningKeyAccess.ts b/server/middlewares/integration/verifyApiKeySiteProvisioningKeyAccess.ts new file mode 100644 index 000000000..d1982ad3c --- /dev/null +++ b/server/middlewares/integration/verifyApiKeySiteProvisioningKeyAccess.ts @@ -0,0 +1,111 @@ +import { Request, Response, NextFunction } from "express"; +import { + db, + siteProvisioningKeys, + siteProvisioningKeyOrg, + apiKeyOrg +} from "@server/db"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import { getFirstString } from "@server/lib/requestParams"; + +export async function verifyApiKeySiteProvisioningKeyAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const siteProvisioningKeyId = + getFirstString(req.params.siteProvisioningKeyId) || + getFirstString(req.body.siteProvisioningKeyId) || + getFirstString(req.query.siteProvisioningKeyId); + const orgId = getFirstString(req.params.orgId); + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!orgId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") + ); + } + + if (!siteProvisioningKeyId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID") + ); + } + + if (apiKey.isRoot) { + // Root keys can access any site provisioning key in any org + return next(); + } + + const [row] = await db + .select() + .from(siteProvisioningKeys) + .innerJoin( + siteProvisioningKeyOrg, + and( + eq( + siteProvisioningKeys.siteProvisioningKeyId, + siteProvisioningKeyOrg.siteProvisioningKeyId + ), + eq(siteProvisioningKeyOrg.orgId, orgId) + ) + ) + .where( + eq( + siteProvisioningKeys.siteProvisioningKeyId, + siteProvisioningKeyId + ) + ) + .limit(1); + + if (!row?.siteProvisioningKeys || !row.siteProvisioningKeyOrg) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site provisioning key with ID ${siteProvisioningKeyId} not found for organization ${orgId}` + ) + ); + } + + if (!req.apiKeyOrg) { + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, row.siteProvisioningKeyOrg.orgId) + ) + ) + .limit(1); + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying site provisioning key access" + ) + ); + } +} diff --git a/server/openApi.ts b/server/openApi.ts index 7132a0327..63625192d 100644 --- a/server/openApi.ts +++ b/server/openApi.ts @@ -18,6 +18,7 @@ export enum OpenAPITags { OrgIdp = "Identity Provider (Organization Only)", Client = "Client", ApiKey = "API Key", + SiteProvisioningKey = "Site Provisioning Key", Domain = "Domain", Blueprint = "Blueprint", Ssh = "SSH", diff --git a/server/private/routers/integration.ts b/server/private/routers/integration.ts index 820a843f0..b613b2f27 100644 --- a/server/private/routers/integration.ts +++ b/server/private/routers/integration.ts @@ -16,6 +16,7 @@ import * as org from "#private/routers/org"; import * as logs from "#private/routers/auditLogs"; import * as alertEvents from "#private/routers/alertEvents"; import * as certificates from "#private/routers/certificates"; +import * as siteProvisioning from "#private/routers/siteProvisioning"; import { verifyApiKeyHasAction, @@ -24,6 +25,7 @@ import { verifyApiKeyIdpAccess, verifyApiKeyRoleAccess, verifyApiKeyUserAccess, + verifyApiKeySiteProvisioningKeyAccess, verifyLimits } from "@server/middlewares"; import * as user from "#private/routers/user"; @@ -215,3 +217,45 @@ authenticated.delete( logActionAudit(ActionsEnum.removeUserRole), user.removeUserRole ); + +authenticated.put( + "/org/:orgId/site-provisioning-key", + verifyValidLicense, + verifyValidSubscription(tierMatrix.siteProvisioningKeys), + verifyApiKeyOrgAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.createSiteProvisioningKey), + logActionAudit(ActionsEnum.createSiteProvisioningKey), + siteProvisioning.createSiteProvisioningKey +); + +authenticated.get( + "/org/:orgId/site-provisioning-keys", + verifyValidLicense, + verifyValidSubscription(tierMatrix.siteProvisioningKeys), + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.listSiteProvisioningKeys), + siteProvisioning.listSiteProvisioningKeys +); + +authenticated.delete( + "/org/:orgId/site-provisioning-key/:siteProvisioningKeyId", + verifyValidLicense, + verifyValidSubscription(tierMatrix.siteProvisioningKeys), + verifyApiKeyOrgAccess, + verifyApiKeySiteProvisioningKeyAccess, + verifyApiKeyHasAction(ActionsEnum.deleteSiteProvisioningKey), + logActionAudit(ActionsEnum.deleteSiteProvisioningKey), + siteProvisioning.deleteSiteProvisioningKey +); + +authenticated.patch( + "/org/:orgId/site-provisioning-key/:siteProvisioningKeyId", + verifyValidLicense, + verifyValidSubscription(tierMatrix.siteProvisioningKeys), + verifyApiKeyOrgAccess, + verifyApiKeySiteProvisioningKeyAccess, + verifyApiKeyHasAction(ActionsEnum.updateSiteProvisioningKey), + logActionAudit(ActionsEnum.updateSiteProvisioningKey), + siteProvisioning.updateSiteProvisioningKey +); diff --git a/server/private/routers/siteProvisioning/createSiteProvisioningKey.ts b/server/private/routers/siteProvisioning/createSiteProvisioningKey.ts index f5b64c0f3..b810ebe4f 100644 --- a/server/private/routers/siteProvisioning/createSiteProvisioningKey.ts +++ b/server/private/routers/siteProvisioning/createSiteProvisioningKey.ts @@ -26,6 +26,8 @@ import { import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; import type { CreateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types"; +import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema"; +import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z.object({ orgId: z.string().nonempty() @@ -34,10 +36,17 @@ const paramsSchema = z.object({ const bodySchema = z .strictObject({ name: z.string().min(1).max(255), - maxBatchSize: z.union([ - z.null(), - z.coerce.number().int().positive().max(1_000_000) - ]), + maxBatchSize: z + .union([ + z.null(), + z.coerce.number().int().positive().max(1_000_000) + ]) + .openapi({ + type: "number", + default: null, + description: + "Maximum number of sites that can be provisioned in a single batch. If null, there is no limit." + }), validUntil: z.string().max(255).optional(), approveNewSites: z.boolean().optional().default(true) }) @@ -57,6 +66,49 @@ const bodySchema = z export type CreateSiteProvisioningKeyBody = z.infer; +const CreateSiteProvisioningKeyResponseDataSchema = z.object({ + siteProvisioningKeyId: z.string(), + orgId: z.string(), + name: z.string(), + siteProvisioningKey: z.string(), + lastChars: z.string(), + createdAt: z.string(), + lastUsed: z.string().nullable(), + maxBatchSize: z.number().nullable(), + numUsed: z.number(), + validUntil: z.string().nullable(), + approveNewSites: z.boolean() +}); + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/site-provisioning-key", + description: "Create a new site provisioning key for the organization.", + tags: [OpenAPITags.SiteProvisioningKey], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: { + 201: { + description: "Successful response", + content: { + "application/json": { + schema: createApiResponseSchema( + CreateSiteProvisioningKeyResponseDataSchema + ) + } + } + } + } +}); + export async function createSiteProvisioningKey( req: Request, res: Response, diff --git a/server/private/routers/siteProvisioning/deleteSiteProvisioningKey.ts b/server/private/routers/siteProvisioning/deleteSiteProvisioningKey.ts index 61fa0a850..9c77c93ca 100644 --- a/server/private/routers/siteProvisioning/deleteSiteProvisioningKey.ts +++ b/server/private/routers/siteProvisioning/deleteSiteProvisioningKey.ts @@ -13,23 +13,46 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { - db, - siteProvisioningKeyOrg, - siteProvisioningKeys -} from "@server/db"; +import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db"; import { and, eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z.object({ siteProvisioningKeyId: z.string().nonempty(), orgId: z.string().nonempty() }); +registry.registerPath({ + method: "delete", + path: "/org/{orgId}/site-provisioning-key/{siteProvisioningKeyId}", + description: "Delete a site provisioning key.", + tags: [OpenAPITags.SiteProvisioningKey], + request: { + params: paramsSchema + }, + responses: { + 200: { + description: "Successful response", + content: { + "application/json": { + schema: z.object({ + data: z.record(z.string(), z.any()).nullable(), + success: z.boolean(), + error: z.boolean(), + message: z.string(), + status: z.number() + }) + } + } + } + } +}); + export async function deleteSiteProvisioningKey( req: Request, res: Response, diff --git a/server/private/routers/siteProvisioning/listSiteProvisioningKeys.ts b/server/private/routers/siteProvisioning/listSiteProvisioningKeys.ts index 0e9838b24..63ab4429a 100644 --- a/server/private/routers/siteProvisioning/listSiteProvisioningKeys.ts +++ b/server/private/routers/siteProvisioning/listSiteProvisioningKeys.ts @@ -11,11 +11,7 @@ * This file is not licensed under the AGPLv3. */ -import { - db, - siteProvisioningKeyOrg, - siteProvisioningKeys -} from "@server/db"; +import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; @@ -25,6 +21,8 @@ import { z } from "zod"; import { fromError } from "zod-validation-error"; import { eq } from "drizzle-orm"; import type { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types"; +import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema"; +import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z.object({ orgId: z.string().nonempty() @@ -45,11 +43,55 @@ const querySchema = z.object({ .pipe(z.int().nonnegative()) }); +const ListSiteProvisioningKeysResponseDataSchema = z.object({ + siteProvisioningKeys: z.array( + z.object({ + siteProvisioningKeyId: z.string(), + orgId: z.string(), + lastChars: z.string(), + createdAt: z.string(), + name: z.string(), + lastUsed: z.string().nullable(), + maxBatchSize: z.number().nullable(), + numUsed: z.number(), + validUntil: z.string().nullable(), + approveNewSites: z.boolean() + }) + ), + pagination: z.object({ + total: z.number(), + limit: z.number(), + offset: z.number() + }) +}); + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/site-provisioning-keys", + description: "List all site provisioning keys for an organization.", + tags: [OpenAPITags.SiteProvisioningKey], + request: { + params: paramsSchema, + query: querySchema + }, + responses: { + 200: { + description: "Successful response", + content: { + "application/json": { + schema: createApiResponseSchema( + ListSiteProvisioningKeysResponseDataSchema + ) + } + } + } + } +}); + function querySiteProvisioningKeys(orgId: string) { return db .select({ - siteProvisioningKeyId: - siteProvisioningKeys.siteProvisioningKeyId, + siteProvisioningKeyId: siteProvisioningKeys.siteProvisioningKeyId, orgId: siteProvisioningKeyOrg.orgId, lastChars: siteProvisioningKeys.lastChars, createdAt: siteProvisioningKeys.createdAt, diff --git a/server/private/routers/siteProvisioning/updateSiteProvisioningKey.ts b/server/private/routers/siteProvisioning/updateSiteProvisioningKey.ts index cee40212a..f97389b2b 100644 --- a/server/private/routers/siteProvisioning/updateSiteProvisioningKey.ts +++ b/server/private/routers/siteProvisioning/updateSiteProvisioningKey.ts @@ -13,11 +13,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { - db, - siteProvisioningKeyOrg, - siteProvisioningKeys -} from "@server/db"; +import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db"; import { and, eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -25,6 +21,8 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import type { UpdateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types"; +import { createApiResponseSchema } from "@server/lib/openapi/createApiResponseSchema"; +import { OpenAPITags, registry } from "@server/openApi"; const paramsSchema = z.object({ siteProvisioningKeyId: z.string().nonempty(), @@ -38,7 +36,13 @@ const bodySchema = z z.null(), z.coerce.number().int().positive().max(1_000_000) ]) - .optional(), + .optional() + .openapi({ + type: "number", + default: null, + description: + "Maximum number of sites that can be provisioned in a single batch. If null, there is no limit." + }), validUntil: z.string().max(255).optional(), approveNewSites: z.boolean().optional() }) @@ -50,7 +54,8 @@ const bodySchema = z ) { ctx.addIssue({ code: "custom", - message: "Provide maxBatchSize and/or validUntil and/or approveNewSites", + message: + "Provide maxBatchSize and/or validUntil and/or approveNewSites", path: ["maxBatchSize"] }); } @@ -69,6 +74,48 @@ const bodySchema = z export type UpdateSiteProvisioningKeyBody = z.infer; +const UpdateSiteProvisioningKeyResponseDataSchema = z.object({ + siteProvisioningKeyId: z.string(), + orgId: z.string(), + name: z.string(), + lastChars: z.string(), + createdAt: z.string(), + lastUsed: z.string().nullable(), + maxBatchSize: z.number().nullable(), + numUsed: z.number(), + validUntil: z.string().nullable(), + approveNewSites: z.boolean() +}); + +registry.registerPath({ + method: "patch", + path: "/org/{orgId}/site-provisioning-key/{siteProvisioningKeyId}", + description: "Update a site provisioning key.", + tags: [OpenAPITags.SiteProvisioningKey], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: { + 200: { + description: "Successful response", + content: { + "application/json": { + schema: createApiResponseSchema( + UpdateSiteProvisioningKeyResponseDataSchema + ) + } + } + } + } +}); + export async function updateSiteProvisioningKey( req: Request, res: Response, diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 48a492c31..48868722e 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -143,6 +143,13 @@ function getActionsCategories(root: boolean) { Logs: { [t("actionExportLogs")]: "exportLogs", [t("actionViewLogs")]: "viewLogs" + }, + + "Site Provisioning Key": { + [t("actionCreateSiteProvisioningKey")]: "createSiteProvisioningKey", + [t("actionListSiteProvisioningKeys")]: "listSiteProvisioningKeys", + [t("actionUpdateSiteProvisioningKey")]: "updateSiteProvisioningKey", + [t("actionDeleteSiteProvisioningKey")]: "deleteSiteProvisioningKey" } }; From a30119f5e875604e050105fcb37a8aeb787e1d58 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 16 Jul 2026 21:28:36 -0400 Subject: [PATCH 02/19] encode email in url param --- server/routers/auth/requestPasswordReset.ts | 2 +- .../user/adminGeneratePasswordResetCode.ts | 2 +- server/routers/user/inviteUser.ts | 4 +-- src/components/InviteStatusCard.tsx | 28 +++++++++++++------ 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/server/routers/auth/requestPasswordReset.ts b/server/routers/auth/requestPasswordReset.ts index 42b53d24d..aca435c7b 100644 --- a/server/routers/auth/requestPasswordReset.ts +++ b/server/routers/auth/requestPasswordReset.ts @@ -99,7 +99,7 @@ export async function requestPasswordReset( }); }); - const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${email}&token=${token}`; + const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${encodeURIComponent(email)}&token=${token}`; if (!config.getRawConfig().email) { logger.info( diff --git a/server/routers/user/adminGeneratePasswordResetCode.ts b/server/routers/user/adminGeneratePasswordResetCode.ts index 562a459e1..7ba8ce9df 100644 --- a/server/routers/user/adminGeneratePasswordResetCode.ts +++ b/server/routers/user/adminGeneratePasswordResetCode.ts @@ -94,7 +94,7 @@ export async function adminGeneratePasswordResetCode( }); }); - const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${existingUser[0].email}&token=${token}`; + const url = `${config.getRawConfig().app.dashboard_url}/auth/reset-password?email=${encodeURIComponent(existingUser[0].email!)}&token=${token}`; logger.info( `Admin generated password reset code for user ${existingUser[0].email} (${userId})` diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 7445ee910..7b7ae9250 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -287,7 +287,7 @@ export async function inviteUser( ) ); - const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`; if (doEmail) { await sendEmail( @@ -341,7 +341,7 @@ export async function inviteUser( .values(uniqueRoleIds.map((roleId) => ({ inviteId, roleId }))); }); - const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`; if (doEmail) { await sendEmail( diff --git a/src/components/InviteStatusCard.tsx b/src/components/InviteStatusCard.tsx index 40b7123e7..790249904 100644 --- a/src/components/InviteStatusCard.tsx +++ b/src/components/InviteStatusCard.tsx @@ -93,14 +93,20 @@ export default function InviteStatusCard({ setType(type); if (!user && type === "user_does_not_exist") { + const inviteRedirect = encodeURIComponent( + `/invite?token=${tokenParam}` + ); const redirectUrl = email - ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}` - : `/auth/signup?redirect=/invite?token=${tokenParam}`; + ? `/auth/signup?redirect=${inviteRedirect}&email=${encodeURIComponent(email)}` + : `/auth/signup?redirect=${inviteRedirect}`; router.push(redirectUrl); } else if (!user && type === "not_logged_in") { + const inviteRedirect = encodeURIComponent( + `/invite?token=${tokenParam}` + ); const redirectUrl = email - ? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}` - : `/auth/login?redirect=/invite?token=${tokenParam}`; + ? `/auth/login?redirect=${inviteRedirect}&user=${encodeURIComponent(email)}` + : `/auth/login?redirect=${inviteRedirect}`; router.push(redirectUrl); } else { setLoading(false); @@ -112,17 +118,23 @@ export default function InviteStatusCard({ async function goToLogin() { await api.post("/auth/logout", {}); + const inviteRedirect = encodeURIComponent( + `/invite?token=${tokenParam}` + ); const redirectUrl = email - ? `/auth/login?redirect=/invite?token=${tokenParam}&user=${email}` - : `/auth/login?redirect=/invite?token=${tokenParam}`; + ? `/auth/login?redirect=${inviteRedirect}&user=${encodeURIComponent(email)}` + : `/auth/login?redirect=${inviteRedirect}`; router.push(redirectUrl); } async function goToSignup() { await api.post("/auth/logout", {}); + const inviteRedirect = encodeURIComponent( + `/invite?token=${tokenParam}` + ); const redirectUrl = email - ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}` - : `/auth/signup?redirect=/invite?token=${tokenParam}`; + ? `/auth/signup?redirect=${inviteRedirect}&email=${encodeURIComponent(email)}` + : `/auth/signup?redirect=${inviteRedirect}`; router.push(redirectUrl); } From 9e50735800d6535f4b97eeb9432af89f91834005 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:29 -0400 Subject: [PATCH 03/19] New translations en-us.json (French) [ci skip] --- messages/fr-FR.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/fr-FR.json b/messages/fr-FR.json index ec24ec4d1..5a931fa70 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Lister les invitations", "actionExportLogs": "Exporter les journaux", "actionViewLogs": "Voir les logs", + "actionCreateSiteProvisioningKey": "Créer une clé de provisionnement de site", + "actionListSiteProvisioningKeys": "Lister les clés de provisionnement de site", + "actionUpdateSiteProvisioningKey": "Mettre à jour la clé de provisionnement de site", + "actionDeleteSiteProvisioningKey": "Supprimer la clé de provisionnement de site", "noneSelected": "Aucune sélection", "orgNotFound2": "Aucune organisation trouvée.", "search": "Rechercher…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Échec du téléchargement", "rdpUnicodeKeyboardMode": "Mode clavier Unicode", "sessionToolbarShow": "Afficher la barre d'outils", - "sessionToolbarHide": "Masquer la barre d'outils" + "sessionToolbarHide": "Masquer la barre d'outils", + "actionUpdateSiteApprovals": "Mettre à jour les approbations de site" } From 6df804ab3247c86667a0af8e4ae9c302a175f01e Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:31 -0400 Subject: [PATCH 04/19] New translations en-us.json (Spanish) [ci skip] --- messages/es-ES.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/es-ES.json b/messages/es-ES.json index d7453592a..e677252a0 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Listar invitaciones", "actionExportLogs": "Exportar registros", "actionViewLogs": "Ver registros", + "actionCreateSiteProvisioningKey": "Crear clave de aprovisionamiento del sitio", + "actionListSiteProvisioningKeys": "Listado de claves de aprovisionamiento del sitio", + "actionUpdateSiteProvisioningKey": "Actualizar clave de aprovisionamiento del sitio", + "actionDeleteSiteProvisioningKey": "Eliminar clave de aprovisionamiento del sitio", "noneSelected": "Ninguno seleccionado", "orgNotFound2": "No se encontraron organizaciones.", "search": "Buscar…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Error de subida", "rdpUnicodeKeyboardMode": "Modo teclado Unicode", "sessionToolbarShow": "Mostrar barra de herramientas", - "sessionToolbarHide": "Ocultar barra de herramientas" + "sessionToolbarHide": "Ocultar barra de herramientas", + "actionUpdateSiteApprovals": "Actualizar aprobaciones del sitio" } From f9bcb25eaaa3ffc274647d4289850a7ce085cefa Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:33 -0400 Subject: [PATCH 05/19] New translations en-us.json (Bulgarian) [ci skip] --- messages/bg-BG.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 25e192fec..4bf0d9ab6 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Списък с покани", "actionExportLogs": "Експортиране на дневници", "actionViewLogs": "Преглед на дневници", + "actionCreateSiteProvisioningKey": "Създаване на ключ за предоставяне на сайт", + "actionListSiteProvisioningKeys": "Списък на ключовете за предоставяне на сайт", + "actionUpdateSiteProvisioningKey": "Актуализиране на ключ за предоставяне на сайт", + "actionDeleteSiteProvisioningKey": "Изтриване на ключ за предоставяне на сайт", "noneSelected": "Нищо не е избрано", "orgNotFound2": "Няма намерени организации.", "search": "Търси…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Качването неуспешно", "rdpUnicodeKeyboardMode": "Режим на unicode клавиатура", "sessionToolbarShow": "Показване на лентата с инструменти", - "sessionToolbarHide": "Скриване на лентата с инструменти" + "sessionToolbarHide": "Скриване на лентата с инструменти", + "actionUpdateSiteApprovals": "Обновяване на одобренията на сайта" } From 61a7dda29a287b8e1b0c787b6bdcec6bad42ba49 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:35 -0400 Subject: [PATCH 06/19] New translations en-us.json (Czech) [ci skip] --- messages/cs-CZ.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index f440a8e38..0d96a80cd 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Seznam pozvánek", "actionExportLogs": "Exportovat protokoly", "actionViewLogs": "Zobrazit logy", + "actionCreateSiteProvisioningKey": "Vytvořit klíč pro zřízení webu", + "actionListSiteProvisioningKeys": "Seznam klíčů pro zřízení webu", + "actionUpdateSiteProvisioningKey": "Aktualizovat klíč pro zřízení webu", + "actionDeleteSiteProvisioningKey": "Smazat klíč pro zřízení webu", "noneSelected": "Není vybráno", "orgNotFound2": "Nebyly nalezeny žádné organizace.", "search": "Vyhledávání…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Nahrání selhalo", "rdpUnicodeKeyboardMode": "Režim Unicode klávesnice", "sessionToolbarShow": "Zobrazit panel nástrojů", - "sessionToolbarHide": "Skrýt panel nástrojů" + "sessionToolbarHide": "Skrýt panel nástrojů", + "actionUpdateSiteApprovals": "Aktualizovat schválení webu" } From 7853f2b096e2fa7ced4761de9f17f4dd0aa57e0e Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:36 -0400 Subject: [PATCH 07/19] New translations en-us.json (Danish) [ci skip] --- messages/da-DK.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/da-DK.json b/messages/da-DK.json index 2e1376264..f88ffa474 100644 --- a/messages/da-DK.json +++ b/messages/da-DK.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Vis invitationer", "actionExportLogs": "Eksportér logs", "actionViewLogs": "Vis logs", + "actionCreateSiteProvisioningKey": "Opret provisioneringsnøgle til site", + "actionListSiteProvisioningKeys": "Vis provisioneringsnøgler til site", + "actionUpdateSiteProvisioningKey": "Opdater provisioneringsnøgle til site", + "actionDeleteSiteProvisioningKey": "Slet provisioneringsnøgle til site", "noneSelected": "Ingen valgt", "orgNotFound2": "Ingen organisationer fundet.", "search": "Søg…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Uploaden mislykkedes", "rdpUnicodeKeyboardMode": "Unicode tastaturtilstand", "sessionToolbarShow": "Vis værktøjslinje", - "sessionToolbarHide": "Skjul værktøjslinje" + "sessionToolbarHide": "Skjul værktøjslinje", + "actionUpdateSiteApprovals": "Opdater godkendelser på site" } From 672555f05130322d31d90de2d8e3ab3e5945b213 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:38 -0400 Subject: [PATCH 08/19] New translations en-us.json (German) [ci skip] --- messages/de-DE.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index 3ceac6dc4..615759bf1 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Einladungen auflisten", "actionExportLogs": "Logs exportieren", "actionViewLogs": "Logs anzeigen", + "actionCreateSiteProvisioningKey": "Bereitstellungsschlüssel für Standort erstellen", + "actionListSiteProvisioningKeys": "Liste der Bereitstellungsschlüssel für Standorte", + "actionUpdateSiteProvisioningKey": "Bereitstellungsschlüssel für Standort aktualisieren", + "actionDeleteSiteProvisioningKey": "Bereitstellungsschlüssel für Standort löschen", "noneSelected": "Keine ausgewählt", "orgNotFound2": "Keine Organisationen gefunden.", "search": "Suche…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Upload fehlgeschlagen", "rdpUnicodeKeyboardMode": "Unicode-Tastaturmodus", "sessionToolbarShow": "Werkzeugleiste zeigen", - "sessionToolbarHide": "Werkzeugleiste ausblenden" + "sessionToolbarHide": "Werkzeugleiste ausblenden", + "actionUpdateSiteApprovals": "Standortgenehmigungen aktualisieren" } From 714e3a8fb1af9bc4505713145318cf072fda9447 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:40 -0400 Subject: [PATCH 09/19] New translations en-us.json (Italian) [ci skip] --- messages/it-IT.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/it-IT.json b/messages/it-IT.json index 5edde2e2e..794eda30f 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Elenco Inviti", "actionExportLogs": "Esporta Log", "actionViewLogs": "Visualizza Log", + "actionCreateSiteProvisioningKey": "Crea Chiave di Provisioning del Sito", + "actionListSiteProvisioningKeys": "Elenca Chiavi di Provisioning del Sito", + "actionUpdateSiteProvisioningKey": "Aggiorna Chiave di Provisioning del Sito", + "actionDeleteSiteProvisioningKey": "Elimina Chiave di Provisioning del Sito", "noneSelected": "Nessuna selezione", "orgNotFound2": "Nessuna organizzazione trovata.", "search": "Cerca…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Caricamento fallito", "rdpUnicodeKeyboardMode": "Modalità tastiera Unicode", "sessionToolbarShow": "Mostra barra degli strumenti", - "sessionToolbarHide": "Nascondi barra degli strumenti" + "sessionToolbarHide": "Nascondi barra degli strumenti", + "actionUpdateSiteApprovals": "Aggiorna Approvazioni del Sito" } From 5f2ee77a73a6566b7ebde44f18f1dbba04e372c1 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:41 -0400 Subject: [PATCH 10/19] New translations en-us.json (Korean) [ci skip] --- messages/ko-KR.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 67c0ee59f..8505e50e0 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "초대 목록", "actionExportLogs": "로그 내보내기", "actionViewLogs": "로그 보기", + "actionCreateSiteProvisioningKey": "사이트 프로비저닝 키 생성", + "actionListSiteProvisioningKeys": "사이트 프로비저닝 키 목록", + "actionUpdateSiteProvisioningKey": "사이트 프로비저닝 키 업데이트", + "actionDeleteSiteProvisioningKey": "사이트 프로비저닝 키 삭제", "noneSelected": "선택된 항목 없음", "orgNotFound2": "조직이 없습니다.", "search": "검색…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "업로드 실패", "rdpUnicodeKeyboardMode": "유니코드 키보드 모드", "sessionToolbarShow": "툴바 보기", - "sessionToolbarHide": "툴바 숨기기" + "sessionToolbarHide": "툴바 숨기기", + "actionUpdateSiteApprovals": "사이트 승인 업데이트" } From 86670c1d6089d7806a9e196fe9f604b7e8d4c956 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:43 -0400 Subject: [PATCH 11/19] New translations en-us.json (Dutch) [ci skip] --- messages/nl-NL.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 9239cd81c..f914a3c80 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Toon uitnodigingen", "actionExportLogs": "Logboeken exporteren", "actionViewLogs": "Logboeken bekijken", + "actionCreateSiteProvisioningKey": "Een sitevoorzie-ningssleutel aanmaken", + "actionListSiteProvisioningKeys": "Sitevoorzie-ningssleutels weergeven", + "actionUpdateSiteProvisioningKey": "Sitevoorzie-ningssleutel bijwerken", + "actionDeleteSiteProvisioningKey": "Sitevoorzie-ningssleutel verwijderen", "noneSelected": "Niet geselecteerd", "orgNotFound2": "Geen organisaties gevonden.", "search": "Zoeken…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Upload mislukt", "rdpUnicodeKeyboardMode": "Unicode toetsenbordmodus", "sessionToolbarShow": "Toon werkbalk", - "sessionToolbarHide": "Verberg werkbalk" + "sessionToolbarHide": "Verberg werkbalk", + "actionUpdateSiteApprovals": "Sitegoedkeuringen bijwerken" } From 3d9c413bbb70acc12cb3246a609889caf3edaea9 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:45 -0400 Subject: [PATCH 12/19] New translations en-us.json (Polish) [ci skip] --- messages/pl-PL.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 11df1b353..1e998dcdd 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Lista zaproszeń", "actionExportLogs": "Eksportuj dzienniki", "actionViewLogs": "Zobacz dzienniki", + "actionCreateSiteProvisioningKey": "Utwórz klucz konfiguracji strony", + "actionListSiteProvisioningKeys": "Lista kluczy konfiguracji strony", + "actionUpdateSiteProvisioningKey": "Zaktualizuj klucz konfiguracji strony", + "actionDeleteSiteProvisioningKey": "Usuń klucz konfiguracji strony", "noneSelected": "Nie wybrano", "orgNotFound2": "Nie znaleziono organizacji.", "search": "Szukaj…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Niepowodzenie przesyłania", "rdpUnicodeKeyboardMode": "Tryb klawiatury Unicode", "sessionToolbarShow": "Pokaż pasek narzędzi", - "sessionToolbarHide": "Ukryj pasek narzędzi" + "sessionToolbarHide": "Ukryj pasek narzędzi", + "actionUpdateSiteApprovals": "Zaktualizuj zgody na stronę" } From 8e09070bc1c2383b250b5bbace3b2e22482ea2c1 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:46 -0400 Subject: [PATCH 13/19] New translations en-us.json (Portuguese) [ci skip] --- messages/pt-PT.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/pt-PT.json b/messages/pt-PT.json index 4a37fb52b..9d3dd1320 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Listar Convites", "actionExportLogs": "Exportar logs", "actionViewLogs": "Visualizar registros", + "actionCreateSiteProvisioningKey": "Criar Chave de Provisionamento do Site", + "actionListSiteProvisioningKeys": "Listar Chaves de Provisionamento do Site", + "actionUpdateSiteProvisioningKey": "Atualizar Chave de Provisionamento do Site", + "actionDeleteSiteProvisioningKey": "Excluir Chave de Provisionamento do Site", "noneSelected": "Nenhum selecionado", "orgNotFound2": "Nenhuma organização encontrada.", "search": "Pesquisar…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Falha no upload", "rdpUnicodeKeyboardMode": "Modo de teclado Unicode", "sessionToolbarShow": "Mostrar barra de ferramentas", - "sessionToolbarHide": "Ocultar barra de ferramentas" + "sessionToolbarHide": "Ocultar barra de ferramentas", + "actionUpdateSiteApprovals": "Atualizar Aprovações do Site" } From e5b9dfee661ce5b844165cb86cfe225c062ca07f Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:48 -0400 Subject: [PATCH 14/19] New translations en-us.json (Russian) [ci skip] --- messages/ru-RU.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/ru-RU.json b/messages/ru-RU.json index efd5bee64..287e61497 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Список приглашений", "actionExportLogs": "Экспорт журналов", "actionViewLogs": "Просмотр журналов", + "actionCreateSiteProvisioningKey": "Создать ключ конфигурации сайта", + "actionListSiteProvisioningKeys": "Список ключей конфигурации сайтов", + "actionUpdateSiteProvisioningKey": "Обновить ключ конфигурации сайта", + "actionDeleteSiteProvisioningKey": "Удалить ключ конфигурации сайта", "noneSelected": "Ничего не выбрано", "orgNotFound2": "Организации не найдены.", "search": "Поиск…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Ошибка загрузки", "rdpUnicodeKeyboardMode": "Режим клавиатуры Unicode", "sessionToolbarShow": "Показать панель инструментов", - "sessionToolbarHide": "Скрыть панель инструментов" + "sessionToolbarHide": "Скрыть панель инструментов", + "actionUpdateSiteApprovals": "Обновить утверждения сайта" } From 50821e972d15c8711f6f0615b8df63e0fdd59654 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:50 -0400 Subject: [PATCH 15/19] New translations en-us.json (Turkish) [ci skip] --- messages/tr-TR.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 006180cc0..55f09d447 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Davetiyeleri Listele", "actionExportLogs": "Kayıtları Dışa Aktar", "actionViewLogs": "Kayıtları Görüntüle", + "actionCreateSiteProvisioningKey": "Site Sağlama Anahtarı Oluştur", + "actionListSiteProvisioningKeys": "Site Sağlama Anahtarlarını Listele", + "actionUpdateSiteProvisioningKey": "Site Sağlama Anahtarını Güncelle", + "actionDeleteSiteProvisioningKey": "Site Sağlama Anahtarını Sil", "noneSelected": "Hiçbiri seçili değil", "orgNotFound2": "Hiçbir organizasyon bulunamadı.", "search": "Ara…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Yükleme başarısız", "rdpUnicodeKeyboardMode": "Unicode klavye modu", "sessionToolbarShow": "Araç çubuğunu göster", - "sessionToolbarHide": "Araç çubuğunu gizle" + "sessionToolbarHide": "Araç çubuğunu gizle", + "actionUpdateSiteApprovals": "Site Onaylarını Güncelle" } From aa9de4d2acde5edbe61dc8e03efe6f84115211b4 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:52 -0400 Subject: [PATCH 16/19] New translations en-us.json (Chinese Simplified) [ci skip] --- messages/zh-CN.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/zh-CN.json b/messages/zh-CN.json index e5a680d99..2f9b8805c 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "邀请列表", "actionExportLogs": "导出日志", "actionViewLogs": "查看日志", + "actionCreateSiteProvisioningKey": "创建站点预配密钥", + "actionListSiteProvisioningKeys": "列出站点预配密钥", + "actionUpdateSiteProvisioningKey": "更新站点预配密钥", + "actionDeleteSiteProvisioningKey": "删除站点预配密钥", "noneSelected": "未选择", "orgNotFound2": "未找到组织。", "search": "搜索…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "上传失败", "rdpUnicodeKeyboardMode": "Unicode 键盘模式", "sessionToolbarShow": "显示工具栏", - "sessionToolbarHide": "隐藏工具栏" + "sessionToolbarHide": "隐藏工具栏", + "actionUpdateSiteApprovals": "更新站点审批" } From a5808200f0840224c6fe14081767423cc5b2ae6f Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Thu, 16 Jul 2026 21:34:54 -0400 Subject: [PATCH 17/19] New translations en-us.json (Norwegian Bokmal) [ci skip] --- messages/nb-NO.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 9bcca74db..8dd687d09 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -1511,6 +1511,10 @@ "actionListInvitations": "Liste invitasjoner", "actionExportLogs": "Eksportlogger", "actionViewLogs": "Vis logger", + "actionCreateSiteProvisioningKey": "Opprett Klargjøringsnøkkel for Sted", + "actionListSiteProvisioningKeys": "List Klargjøringsnøkler for Sted", + "actionUpdateSiteProvisioningKey": "Oppdater Klargjøringsnøkkel for Sted", + "actionDeleteSiteProvisioningKey": "Slett Klargjøringsnøkkel for Sted", "noneSelected": "Ingen valgt", "orgNotFound2": "Ingen organisasjoner funnet.", "search": "Søk…", @@ -3811,5 +3815,6 @@ "rdpUploadFailed": "Opplastningen mislyktes", "rdpUnicodeKeyboardMode": "Unicode tastaturmodus", "sessionToolbarShow": "Vis verktøylinje", - "sessionToolbarHide": "Skjul verktøylinje" + "sessionToolbarHide": "Skjul verktøylinje", + "actionUpdateSiteApprovals": "Oppdater Stedsgodkjenninger" } From 2d48adc9f9238c94152017156b5494245d7a7f71 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 16 Jul 2026 22:06:43 -0400 Subject: [PATCH 18/19] use proxy for logout on org auth page session expire --- server/routers/auth/logout.ts | 25 ++++++++++++--------- src/actions/server.ts | 33 ++++++++++++++++++++++++++++ src/components/OrgPolicyRequired.tsx | 27 +++++++++++++---------- 3 files changed, 63 insertions(+), 22 deletions(-) diff --git a/server/routers/auth/logout.ts b/server/routers/auth/logout.ts index b9a1431aa..b36d0c73a 100644 --- a/server/routers/auth/logout.ts +++ b/server/routers/auth/logout.ts @@ -16,18 +16,26 @@ export async function logout( next: NextFunction ): Promise { const { user, session } = await verifySession(req); + const isSecure = req.protocol === "https"; + + // Always clear the session cookie so logout is idempotent, even when + // the session is already missing or invalid + res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure)); + if (!user || !session) { if (config.getRawConfig().app.log_failed_attempts) { logger.info( - `Log out failed because missing or invalid session. IP: ${req.ip}.` + `Log out with missing or invalid session. IP: ${req.ip}.` ); } - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "You must be logged in to sign out" - ) - ); + + return response(res, { + data: null, + success: true, + error: false, + message: "Logged out successfully", + status: HttpCode.OK + }); } try { @@ -37,9 +45,6 @@ export async function logout( logger.error("Failed to invalidate session", error); } - const isSecure = req.protocol === "https"; - res.setHeader("Set-Cookie", createBlankSessionTokenCookie(isSecure)); - return response(res, { data: null, success: true, diff --git a/src/actions/server.ts b/src/actions/server.ts index 2759e6213..cd75c2c57 100644 --- a/src/actions/server.ts +++ b/src/actions/server.ts @@ -248,6 +248,39 @@ export async function loginProxy( return await makeApiRequest(url, "POST", request); } +export async function logoutProxy(): Promise> { + const env = pullEnv(); + const serverPort = process.env.SERVER_EXTERNAL_PORT; + const url = `http://localhost:${serverPort}/api/v1/auth/logout`; + + const result = await makeApiRequest(url, "POST"); + + try { + const headersList = await reqHeaders(); + const host = headersList.get("host")?.split(":")[0]; + const allCookies = await cookies(); + const clearOptions = { + httpOnly: true, + secure: true, + sameSite: "lax" as const, + path: "/", + maxAge: 0 + }; + // Clear both host-only and domain-scoped variants. + allCookies.set(env.server.sessionCookieName, "", clearOptions); + if (host) { + allCookies.set(env.server.sessionCookieName, "", { + ...clearOptions, + domain: host + }); + } + } catch (cookieError) { + console.error("Failed to clear session cookie:", cookieError); + } + + return result; +} + export async function securityKeyStartProxy( request: SecurityKeyStartRequest, forceLogin?: boolean diff --git a/src/components/OrgPolicyRequired.tsx b/src/components/OrgPolicyRequired.tsx index 3765cd1bf..6d28ddbf5 100644 --- a/src/components/OrgPolicyRequired.tsx +++ b/src/components/OrgPolicyRequired.tsx @@ -12,8 +12,8 @@ import { Shield, ArrowRight } from "lucide-react"; import Link from "next/link"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useState } from "react"; +import { logoutProxy } from "@app/actions/server"; type OrgPolicyRequiredProps = { orgId: string; @@ -40,21 +40,23 @@ export default function OrgPolicyRequired({ }: OrgPolicyRequiredProps) { const t = useTranslations(); const router = useRouter(); - - const api = createApiClient(useEnvContext()); + const [loading, setLoading] = useState(false); const sessionExpired = policies?.maxSessionLength && policies.maxSessionLength.compliant === false; - function reauthenticate() { - api.post("/auth/logout") - .catch(() => {}) - .then(() => { - const destination = redirectAfterAuth ?? `/${orgId}`; - router.push(destination); - router.refresh(); - }); + async function reauthenticate() { + setLoading(true); + try { + await logoutProxy(); + } catch (error) { + console.error("Error during logout:", error); + } finally { + const destination = redirectAfterAuth ?? `/${orgId}`; + router.push(destination); + router.refresh(); + } } if (sessionExpired) { @@ -76,6 +78,7 @@ export default function OrgPolicyRequired({