From ab4c105e757201136af0462dfe5738c505cc0293 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 19 Oct 2026 16:55:10 -0400 Subject: [PATCH 01/26] Make rekey not private and in integration api --- messages/en-US.json | 1 + server/lib/backupFileName.ts | 11 ---- server/openApi.ts | 3 +- server/private/routers/external.ts | 31 --------- server/private/routers/re-key/index.ts | 16 ----- .../resource/createResourceMtlsCertificate.ts | 13 ++++ .../resource/deleteResourceMtlsCertificate.ts | 13 ++++ .../resource/listResourceMtlsCertificates.ts | 13 ++++ server/routers/external.ts | 25 +++++++ server/routers/integration.ts | 28 ++++++++ server/routers/re-key/index.ts | 3 + .../routers/re-key/reGenerateClientSecret.ts | 49 ++++++++++---- .../re-key/reGenerateExitNodeSecret.ts | 66 ++++++++++++++----- .../routers/re-key/reGenerateSiteSecret.ts | 49 ++++++++++---- .../[remoteExitNodeId]/credentials/page.tsx | 15 ----- .../machine/[niceId]/credentials/page.tsx | 15 ----- .../sites/[niceId]/credentials/page.tsx | 32 +-------- src/components/PermissionsSelectBox.tsx | 3 +- 18 files changed, 221 insertions(+), 165 deletions(-) delete mode 100644 server/private/routers/re-key/index.ts create mode 100644 server/routers/re-key/index.ts rename server/{private => }/routers/re-key/reGenerateClientSecret.ts (80%) rename server/{private => }/routers/re-key/reGenerateExitNodeSecret.ts (68%) rename server/{private => }/routers/re-key/reGenerateSiteSecret.ts (86%) diff --git a/messages/en-US.json b/messages/en-US.json index 760a42a24..a6aa0060f 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1504,6 +1504,7 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "actionReGenerateSecret": "Regenerate Credentials", "actionCreateAiProvider": "Create AI Provider", "actionDeleteAiProvider": "Delete AI Provider", "actionGetAiProvider": "Get AI Provider", diff --git a/server/lib/backupFileName.ts b/server/lib/backupFileName.ts index 9d2c1bd62..69b08dc8b 100644 --- a/server/lib/backupFileName.ts +++ b/server/lib/backupFileName.ts @@ -1,14 +1,3 @@ -/** - * Builds the timestamp segment of a database backup file name. - * - * `Date#getMonth` is zero-indexed, so building this inline produced names like - * `db_2026-8-12_...` for a backup taken on 12 September 2026. Every field is - * also zero-padded, which keeps the names unambiguous and makes them sort - * lexicographically in the order they were taken. - * - * @param date The moment the backup is being taken. Defaults to now. - * @returns A timestamp of the form `YYYY-MM-DD_HH-MM-SS`. - */ export function formatBackupTimestamp(date: Date = new Date()): string { const pad = (value: number): string => String(value).padStart(2, "0"); diff --git a/server/openApi.ts b/server/openApi.ts index 94e2079d0..3c77ed979 100644 --- a/server/openApi.ts +++ b/server/openApi.ts @@ -34,7 +34,8 @@ export enum OpenAPITags { AiBudget = "AI Budget", VirtualApiKey = "Virtual API Key", Redirect = "Redirect", - Label = "Label" + Label = "Label", + RemoteExitNode = "Remote Exit Node" } // Order here controls the order tags are displayed in Swagger UI diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 720183a36..6dcf0e3be 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -26,7 +26,6 @@ import { exportAiSessionLogs } from "@server/routers/auditLogs"; import * as misc from "#private/routers/misc"; -import * as reKey from "#private/routers/re-key"; import * as approval from "#private/routers/approvals"; import * as user from "#private/routers/user"; import * as client from "#private/routers/client"; @@ -617,36 +616,6 @@ authenticated.get( exportAiSessionLogs ); -authenticated.post( - "/re-key/:clientId/regenerate-client-secret", - verifyClientAccess, // this is first to set the org id - verifyValidLicense, - verifyValidSubscription(tierMatrix.rotateCredentials), - verifyLimits, - verifyUserHasAction(ActionsEnum.reGenerateSecret), - reKey.reGenerateClientSecret -); - -authenticated.post( - "/re-key/:siteId/regenerate-site-secret", - verifySiteAccess, // this is first to set the org id - verifyValidLicense, - verifyValidSubscription(tierMatrix.rotateCredentials), - verifyLimits, - verifyUserHasAction(ActionsEnum.reGenerateSecret), - reKey.reGenerateSiteSecret -); - -authenticated.put( - "/re-key/:orgId/regenerate-remote-exit-node-secret", - verifyValidLicense, - verifyValidSubscription(tierMatrix.rotateCredentials), - verifyOrgAccess, - verifyLimits, - verifyUserHasAction(ActionsEnum.reGenerateSecret), - reKey.reGenerateExitNodeSecret -); - authenticated.post( "/user/:userId/add-role/:roleId", verifyRoleAccess, diff --git a/server/private/routers/re-key/index.ts b/server/private/routers/re-key/index.ts deleted file mode 100644 index 922a021fd..000000000 --- a/server/private/routers/re-key/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - -export * from "./reGenerateClientSecret"; -export * from "./reGenerateSiteSecret"; -export * from "./reGenerateExitNodeSecret"; diff --git a/server/private/routers/resource/createResourceMtlsCertificate.ts b/server/private/routers/resource/createResourceMtlsCertificate.ts index 5ad04b55d..d42293441 100644 --- a/server/private/routers/resource/createResourceMtlsCertificate.ts +++ b/server/private/routers/resource/createResourceMtlsCertificate.ts @@ -1,3 +1,16 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, resourceMtlsCertificates, resources } from "@server/db"; diff --git a/server/private/routers/resource/deleteResourceMtlsCertificate.ts b/server/private/routers/resource/deleteResourceMtlsCertificate.ts index f9ee3c2f6..c74f0eb1f 100644 --- a/server/private/routers/resource/deleteResourceMtlsCertificate.ts +++ b/server/private/routers/resource/deleteResourceMtlsCertificate.ts @@ -1,3 +1,16 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, resourceMtlsCertificates, resources } from "@server/db"; diff --git a/server/private/routers/resource/listResourceMtlsCertificates.ts b/server/private/routers/resource/listResourceMtlsCertificates.ts index 142d225b7..aa6ab8b24 100644 --- a/server/private/routers/resource/listResourceMtlsCertificates.ts +++ b/server/private/routers/resource/listResourceMtlsCertificates.ts @@ -1,3 +1,16 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, resourceMtlsCertificates, resources } from "@server/db"; diff --git a/server/routers/external.ts b/server/routers/external.ts index d5faba7cd..71691a336 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -22,6 +22,7 @@ import * as newt from "./newt"; import * as olm from "./olm"; import * as ssh from "./ssh"; import * as serverInfo from "./serverInfo"; +import * as reKey from "./re-key"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, @@ -1972,6 +1973,30 @@ authenticated.post( certificates.restartCertificate ); +authenticated.post( + "/re-key/:clientId/regenerate-client-secret", + verifyClientAccess, // this is first to set the org id + verifyLimits, + verifyUserHasAction(ActionsEnum.reGenerateSecret), + reKey.reGenerateClientSecret +); + +authenticated.post( + "/re-key/:siteId/regenerate-site-secret", + verifySiteAccess, // this is first to set the org id + verifyLimits, + verifyUserHasAction(ActionsEnum.reGenerateSecret), + reKey.reGenerateSiteSecret +); + +authenticated.put( + "/re-key/:orgId/regenerate-remote-exit-node-secret", + verifyOrgAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.reGenerateSecret), + reKey.reGenerateExitNodeSecret +); + // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter); diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 20b6d9c78..dac082434 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -18,6 +18,7 @@ import * as aiBudget from "./aiBudget"; import * as virtualApiKey from "./virtualApiKey"; import * as redirect from "./redirect"; import * as labels from "./labels"; +import * as reKey from "./re-key"; import { verifyApiKey, verifyApiKeyOrgAccess, @@ -149,6 +150,24 @@ authenticated.post( site.updateSite ); +authenticated.post( + "/re-key/:siteId/regenerate-site-secret", + verifyApiKeySiteAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.reGenerateSecret), + logActionAudit(ActionsEnum.reGenerateSecret), + reKey.reGenerateSiteSecret +); + +authenticated.put( + "/re-key/:orgId/regenerate-remote-exit-node-secret", + verifyApiKeyOrgAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.reGenerateSecret), + logActionAudit(ActionsEnum.reGenerateSecret), + reKey.reGenerateExitNodeSecret +); + authenticated.post( "/org/:orgId/reset-bandwidth", verifyApiKeyOrgAccess, @@ -1497,6 +1516,15 @@ authenticated.post( client.updateClient ); +authenticated.post( + "/re-key/:clientId/regenerate-client-secret", + verifyApiKeyClientAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.reGenerateSecret), + logActionAudit(ActionsEnum.reGenerateSecret), + reKey.reGenerateClientSecret +); + authenticated.put( "/org/:orgId/blueprint", verifyApiKeyOrgAccess, diff --git a/server/routers/re-key/index.ts b/server/routers/re-key/index.ts new file mode 100644 index 000000000..beb38a92e --- /dev/null +++ b/server/routers/re-key/index.ts @@ -0,0 +1,3 @@ +export * from "./reGenerateClientSecret"; +export * from "./reGenerateSiteSecret"; +export * from "./reGenerateExitNodeSecret"; diff --git a/server/private/routers/re-key/reGenerateClientSecret.ts b/server/routers/re-key/reGenerateClientSecret.ts similarity index 80% rename from server/private/routers/re-key/reGenerateClientSecret.ts rename to server/routers/re-key/reGenerateClientSecret.ts index c0c160f7e..53217de24 100644 --- a/server/private/routers/re-key/reGenerateClientSecret.ts +++ b/server/routers/re-key/reGenerateClientSecret.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, Olm, olms } from "@server/db"; @@ -23,7 +10,7 @@ import { eq, and } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { hashPassword } from "@server/auth/password"; -import { disconnectClient, sendToClient } from "#private/routers/ws"; +import { disconnectClient, sendToClient } from "#dynamic/routers/ws"; import { OlmErrorCodes, sendOlmError } from "@server/routers/olm/error"; import { sendTerminateClient } from "@server/routers/client/terminate"; @@ -39,6 +26,40 @@ const reGenerateSecretBodySchema = z.strictObject({ export type ReGenerateSecretBody = z.infer; +registry.registerPath({ + method: "post", + path: "/re-key/{clientId}/regenerate-client-secret", + description: + "Regenerate the secret for a machine client's olm. Optionally disconnects the olm so it must reconnect with the new secret.", + tags: [OpenAPITags.Client], + request: { + params: reGenerateSecretParamsSchema, + body: { + content: { + "application/json": { + schema: reGenerateSecretBodySchema + } + } + } + }, + responses: { + 200: { + description: "Successful response", + content: { + "application/json": { + schema: z.object({ + data: z.object({ olmId: z.string() }), + success: z.boolean(), + error: z.boolean(), + message: z.string(), + status: z.number() + }) + } + } + } + } +}); + export async function reGenerateClientSecret( req: Request, res: Response, diff --git a/server/private/routers/re-key/reGenerateExitNodeSecret.ts b/server/routers/re-key/reGenerateExitNodeSecret.ts similarity index 68% rename from server/private/routers/re-key/reGenerateExitNodeSecret.ts rename to server/routers/re-key/reGenerateExitNodeSecret.ts index df5f57048..80bcb166d 100644 --- a/server/private/routers/re-key/reGenerateExitNodeSecret.ts +++ b/server/routers/re-key/reGenerateExitNodeSecret.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { NextFunction, Request, Response } from "express"; import { db, @@ -30,7 +17,7 @@ import { hashPassword } from "@server/auth/password"; import logger from "@server/logger"; import { and, eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -import { disconnectClient, sendToClient } from "#private/routers/ws"; +import { disconnectClient, sendToClient } from "#dynamic/routers/ws"; export const paramsSchema = z.object({ orgId: z.string() @@ -42,6 +29,40 @@ const bodySchema = z.strictObject({ disconnect: z.boolean().optional().default(true) }); +registry.registerPath({ + method: "put", + path: "/re-key/{orgId}/regenerate-remote-exit-node-secret", + description: + "Regenerate the secret for a remote exit node. Optionally disconnects the node so it must reconnect with the new secret.", + tags: [OpenAPITags.RemoteExitNode], + request: { + params: paramsSchema, + body: { + content: { + "application/json": { + schema: bodySchema + } + } + } + }, + responses: { + 200: { + description: "Successful response", + content: { + "application/json": { + schema: z.object({ + data: z.null(), + success: z.boolean(), + error: z.boolean(), + message: z.string(), + status: z.number() + }) + } + } + } + } +}); + export async function reGenerateExitNodeSecret( req: Request, res: Response, @@ -68,12 +89,25 @@ export async function reGenerateExitNodeSecret( ); } + const { orgId } = parsedParams.data; const { remoteExitNodeId, secret, disconnect } = parsedBody.data; + // Only allow re-keying a node that belongs to this org (a node can be + // shared across several orgs; any of them may re-key it) const [existingRemoteExitNode] = await db - .select() + .select({ remoteExitNodeId: remoteExitNodes.remoteExitNodeId }) .from(remoteExitNodes) - .where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId)); + .innerJoin( + exitNodeOrgs, + eq(exitNodeOrgs.exitNodeId, remoteExitNodes.exitNodeId) + ) + .where( + and( + eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId), + eq(exitNodeOrgs.orgId, orgId) + ) + ) + .limit(1); if (!existingRemoteExitNode) { return next( diff --git a/server/private/routers/re-key/reGenerateSiteSecret.ts b/server/routers/re-key/reGenerateSiteSecret.ts similarity index 86% rename from server/private/routers/re-key/reGenerateSiteSecret.ts rename to server/routers/re-key/reGenerateSiteSecret.ts index 2bb29b202..3afbe58a3 100644 --- a/server/private/routers/re-key/reGenerateSiteSecret.ts +++ b/server/routers/re-key/reGenerateSiteSecret.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, Newt, newts, sites } from "@server/db"; @@ -24,7 +11,7 @@ import { OpenAPITags, registry } from "@server/openApi"; import { hashPassword } from "@server/auth/password"; import { addPeer, deletePeer } from "@server/routers/gerbil/peers"; import { getAllowedIps } from "@server/routers/target/helpers"; -import { disconnectClient, sendToClient } from "#private/routers/ws"; +import { disconnectClient, sendToClient } from "#dynamic/routers/ws"; const updateSiteParamsSchema = z.strictObject({ siteId: z.coerce.number().int().positive() @@ -37,6 +24,40 @@ const updateSiteBodySchema = z.strictObject({ disconnect: z.boolean().optional().default(true) }); +registry.registerPath({ + method: "post", + path: "/re-key/{siteId}/regenerate-site-secret", + description: + "Regenerate the credentials for a site. For newt sites a new secret is required; for wireguard sites a new public key is required.", + tags: [OpenAPITags.Site], + request: { + params: updateSiteParamsSchema, + body: { + content: { + "application/json": { + schema: updateSiteBodySchema + } + } + } + }, + responses: { + 200: { + description: "Successful response", + content: { + "application/json": { + schema: z.object({ + data: z.object({ newtId: z.string().optional() }), + success: z.boolean(), + error: z.boolean(), + message: z.string(), + status: z.number() + }) + } + } + } + } +}); + export async function reGenerateSiteSecret( req: Request, res: Response, diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx index 2f24b15cf..24efde738 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/credentials/page.tsx @@ -32,9 +32,6 @@ import { import CopyToClipboard from "@app/components/CopyToClipboard"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; -import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; -import { usePaidStatus } from "@app/hooks/usePaidStatus"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -44,8 +41,6 @@ export default function CredentialsPage() { const t = useTranslations(); const { remoteExitNode } = useRemoteExitNodeContext(); - const { isPaidUser } = usePaidStatus(); - const [modalOpen, setModalOpen] = useState(false); const [credentials, setCredentials] = useState(null); @@ -129,10 +124,6 @@ export default function CredentialsPage() { - - @@ -196,9 +187,6 @@ export default function CredentialsPage() { setShouldDisconnect(false); setModalOpen(true); }} - disabled={ - !isPaidUser(tierMatrix.rotateCredentials) - } > {t("regenerateCredentialsButton")} @@ -207,9 +195,6 @@ export default function CredentialsPage() { setShouldDisconnect(true); setModalOpen(true); }} - disabled={ - !isPaidUser(tierMatrix.rotateCredentials) - } > {t("remoteExitNodeRegenerateAndDisconnect")} diff --git a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx index 634994b25..c575a53b2 100644 --- a/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/credentials/page.tsx @@ -28,10 +28,7 @@ import { import CopyToClipboard from "@app/components/CopyToClipboard"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; -import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { OlmInstallCommands } from "@app/components/olm-install-commands"; -import { usePaidStatus } from "@app/hooks/usePaidStatus"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -53,8 +50,6 @@ export default function CredentialsPage() { const [showCredentialsAlert, setShowCredentialsAlert] = useState(false); const [shouldDisconnect, setShouldDisconnect] = useState(true); - const { isPaidUser } = usePaidStatus(); - const handleConfirmRegenerate = async () => { try { const res = await api.get(`/org/${orgId}/pick-client-defaults`); @@ -119,10 +114,6 @@ export default function CredentialsPage() { - - @@ -182,9 +173,6 @@ export default function CredentialsPage() { setShouldDisconnect(false); setModalOpen(true); }} - disabled={ - !isPaidUser(tierMatrix.rotateCredentials) - } > {t("regenerateCredentialsButton")} @@ -193,9 +181,6 @@ export default function CredentialsPage() { setShouldDisconnect(true); setModalOpen(true); }} - disabled={ - !isPaidUser(tierMatrix.rotateCredentials) - } > {t("clientRegenerateAndDisconnect")} diff --git a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx index 88ab47bb0..72bea2ccf 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx @@ -35,10 +35,7 @@ import { generateObfuscatedWireGuardConfig } from "@app/lib/wireguard"; import { QRCodeCanvas } from "qrcode.react"; -import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { NewtSiteInstallCommands } from "@app/components/newt-install-commands"; -import { usePaidStatus } from "@app/hooks/usePaidStatus"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; import type { AxiosResponse } from "axios"; import { useQuery } from "@tanstack/react-query"; import { productUpdatesQueries } from "@app/lib/queries"; @@ -67,8 +64,6 @@ export default function CredentialsPage() { const [loadingDefaults, setLoadingDefaults] = useState(false); const [shouldDisconnect, setShouldDisconnect] = useState(true); - const { isPaidUser } = usePaidStatus(); - const { data: latestVersions } = useQuery( productUpdatesQueries.latestVersion(true) ); @@ -206,10 +201,6 @@ export default function CredentialsPage() { - - @@ -280,11 +271,6 @@ export default function CredentialsPage() { setShouldDisconnect(false); setModalOpen(true); }} - disabled={ - !isPaidUser( - tierMatrix.rotateCredentials - ) - } > {t("regenerateCredentialsButton")} @@ -293,11 +279,6 @@ export default function CredentialsPage() { setShouldDisconnect(true); setModalOpen(true); }} - disabled={ - !isPaidUser( - tierMatrix.rotateCredentials - ) - } > {t("siteRegenerateAndDisconnect")} @@ -324,10 +305,6 @@ export default function CredentialsPage() { - - {!loadingDefaults && ( <> @@ -393,14 +370,7 @@ export default function CredentialsPage() { {!env.flags.disableEnterpriseFeatures && ( - diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index a6bb26ec5..19b39eab9 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -36,7 +36,8 @@ function getActionsCategories(root: boolean) { [t("createOrgUser")]: "createOrgUser", [t("actionApplyBlueprint")]: "applyBlueprint", [t("actionListBlueprints")]: "listBlueprints", - [t("actionGetBlueprint")]: "getBlueprint" + [t("actionGetBlueprint")]: "getBlueprint", + [t("actionReGenerateSecret")]: "reGenerateSecret" }, Domain: { From a56e67f8521ca92ddf2903c7d531ceafa1885a31 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 19 Oct 2026 16:58:45 -0400 Subject: [PATCH 02/26] Move mtls out of private --- server/private/routers/external.ts | 24 -------------- server/private/routers/integration.ts | 33 ------------------- server/private/routers/resource/index.ts | 3 -- server/routers/external.ts | 22 +++++++++++++ server/routers/integration.ts | 33 +++++++++++++++++++ .../resource/createResourceMtlsCertificate.ts | 13 -------- .../resource/deleteResourceMtlsCertificate.ts | 13 -------- server/routers/resource/index.ts | 3 ++ .../resource/listResourceMtlsCertificates.ts | 13 -------- .../resources/public/[niceId]/mtls/page.tsx | 7 +--- 10 files changed, 59 insertions(+), 105 deletions(-) rename server/{private => }/routers/resource/createResourceMtlsCertificate.ts (95%) rename server/{private => }/routers/resource/deleteResourceMtlsCertificate.ts (91%) rename server/{private => }/routers/resource/listResourceMtlsCertificates.ts (90%) diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 6dcf0e3be..cad76b80d 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -34,8 +34,6 @@ import * as eventStreamingDestination from "#private/routers/eventStreamingDesti import * as alertRule from "#private/routers/alertRule"; import * as healthChecks from "#private/routers/healthChecks"; import * as policy from "#private/routers/policy"; -import * as resource from "#private/routers/resource"; - import { verifyOrgAccess, verifyUserHasAction, @@ -864,25 +862,3 @@ authenticated.post( verifyOrgAccess, logs.logAccessAuditAttempt ); - -authenticated.put( - "/resource/:resourceId/mtls/certificate", - verifyResourceAccess, - verifyLimits, - verifyUserHasAction(ActionsEnum.createResourceMtlsCertificate), - logActionAudit(ActionsEnum.createResourceMtlsCertificate), - resource.createResourceMtlsCertificate -); -authenticated.get( - "/resource/:resourceId/mtls/certificates", - verifyResourceAccess, - verifyUserHasAction(ActionsEnum.listResourceMtlsCertificates), - resource.listResourceMtlsCertificates -); -authenticated.delete( - "/resource/:resourceId/mtls/certificate/:mtlsCertificateId", - verifyResourceAccess, - verifyUserHasAction(ActionsEnum.deleteResourceMtlsCertificate), - logActionAudit(ActionsEnum.deleteResourceMtlsCertificate), - resource.deleteResourceMtlsCertificate -); diff --git a/server/private/routers/integration.ts b/server/private/routers/integration.ts index c4bbe4160..9d92d3481 100644 --- a/server/private/routers/integration.ts +++ b/server/private/routers/integration.ts @@ -467,36 +467,3 @@ authenticated.get( verifyApiKeyHasAction(ActionsEnum.getTarget), healthChecks.getHealthCheckStatusHistory ); - -authenticated.put( - [ - "/resource/:resourceId/mtls/certificate", - "/public-resource/:resourceId/mtls/certificate" - ], - verifyApiKeyResourceAccess, - verifyLimits, - verifyApiKeyHasAction(ActionsEnum.createResourceMtlsCertificate), - logActionAudit(ActionsEnum.createResourceMtlsCertificate), - resource.createResourceMtlsCertificate -); - -authenticated.get( - [ - "/resource/:resourceId/mtls/certificates", - "/public-resource/:resourceId/mtls/certificates" - ], - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.listResourceMtlsCertificates), - resource.listResourceMtlsCertificates -); - -authenticated.delete( - [ - "/resource/:resourceId/mtls/certificate/:mtlsCertificateId", - "/public-resource/:resourceId/mtls/certificate/:mtlsCertificateId" - ], - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.deleteResourceMtlsCertificate), - logActionAudit(ActionsEnum.deleteResourceMtlsCertificate), - resource.deleteResourceMtlsCertificate -); diff --git a/server/private/routers/resource/index.ts b/server/private/routers/resource/index.ts index 6cf21f900..ea9b87d65 100644 --- a/server/private/routers/resource/index.ts +++ b/server/private/routers/resource/index.ts @@ -12,6 +12,3 @@ */ export * from "./getMaintenanceInfo"; -export * from "./deleteResourceMtlsCertificate"; -export * from "./createResourceMtlsCertificate"; -export * from "./listResourceMtlsCertificates"; diff --git a/server/routers/external.ts b/server/routers/external.ts index 71691a336..7640e98da 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -1997,6 +1997,28 @@ authenticated.put( reKey.reGenerateExitNodeSecret ); +authenticated.put( + "/resource/:resourceId/mtls/certificate", + verifyResourceAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.createResourceMtlsCertificate), + logActionAudit(ActionsEnum.createResourceMtlsCertificate), + resource.createResourceMtlsCertificate +); +authenticated.get( + "/resource/:resourceId/mtls/certificates", + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.listResourceMtlsCertificates), + resource.listResourceMtlsCertificates +); +authenticated.delete( + "/resource/:resourceId/mtls/certificate/:mtlsCertificateId", + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.deleteResourceMtlsCertificate), + logActionAudit(ActionsEnum.deleteResourceMtlsCertificate), + resource.deleteResourceMtlsCertificate +); + // Auth routes export const authRouter = Router(); unauthenticated.use("/auth", authRouter); diff --git a/server/routers/integration.ts b/server/routers/integration.ts index dac082434..424bbd213 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -1963,3 +1963,36 @@ authenticated.put( logActionAudit(ActionsEnum.detachLabelFromItem), labels.detachLabelFromItem ); + +authenticated.put( + [ + "/resource/:resourceId/mtls/certificate", + "/public-resource/:resourceId/mtls/certificate" + ], + verifyApiKeyResourceAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.createResourceMtlsCertificate), + logActionAudit(ActionsEnum.createResourceMtlsCertificate), + resource.createResourceMtlsCertificate +); + +authenticated.get( + [ + "/resource/:resourceId/mtls/certificates", + "/public-resource/:resourceId/mtls/certificates" + ], + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.listResourceMtlsCertificates), + resource.listResourceMtlsCertificates +); + +authenticated.delete( + [ + "/resource/:resourceId/mtls/certificate/:mtlsCertificateId", + "/public-resource/:resourceId/mtls/certificate/:mtlsCertificateId" + ], + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.deleteResourceMtlsCertificate), + logActionAudit(ActionsEnum.deleteResourceMtlsCertificate), + resource.deleteResourceMtlsCertificate +); diff --git a/server/private/routers/resource/createResourceMtlsCertificate.ts b/server/routers/resource/createResourceMtlsCertificate.ts similarity index 95% rename from server/private/routers/resource/createResourceMtlsCertificate.ts rename to server/routers/resource/createResourceMtlsCertificate.ts index d42293441..5ad04b55d 100644 --- a/server/private/routers/resource/createResourceMtlsCertificate.ts +++ b/server/routers/resource/createResourceMtlsCertificate.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, resourceMtlsCertificates, resources } from "@server/db"; diff --git a/server/private/routers/resource/deleteResourceMtlsCertificate.ts b/server/routers/resource/deleteResourceMtlsCertificate.ts similarity index 91% rename from server/private/routers/resource/deleteResourceMtlsCertificate.ts rename to server/routers/resource/deleteResourceMtlsCertificate.ts index c74f0eb1f..f9ee3c2f6 100644 --- a/server/private/routers/resource/deleteResourceMtlsCertificate.ts +++ b/server/routers/resource/deleteResourceMtlsCertificate.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, resourceMtlsCertificates, resources } from "@server/db"; diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 7c888004c..c4004fe1b 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -45,3 +45,6 @@ export * from "./addAiProviderToResource"; export * from "./removeAiProviderFromResource"; export * from "./exportResourceBlueprint"; export * from "./types"; +export * from "./deleteResourceMtlsCertificate"; +export * from "./createResourceMtlsCertificate"; +export * from "./listResourceMtlsCertificates"; diff --git a/server/private/routers/resource/listResourceMtlsCertificates.ts b/server/routers/resource/listResourceMtlsCertificates.ts similarity index 90% rename from server/private/routers/resource/listResourceMtlsCertificates.ts rename to server/routers/resource/listResourceMtlsCertificates.ts index aa6ab8b24..142d225b7 100644 --- a/server/private/routers/resource/listResourceMtlsCertificates.ts +++ b/server/routers/resource/listResourceMtlsCertificates.ts @@ -1,16 +1,3 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025-2026 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, resourceMtlsCertificates, resources } from "@server/db"; diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx index 7fd454d31..955eb041c 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx @@ -16,14 +16,11 @@ import { import { SwitchInput } from "@app/components/SwitchInput"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Badge } from "@app/components/ui/badge"; -import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { resourceQueries } from "@app/lib/queries"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; import type { CreateResourceMtlsCertificateResponse, DeleteResourceMtlsCertificateResponse, @@ -47,10 +44,9 @@ export default function ResourceMtlsPage() { const t = useTranslations(); const api = createApiClient({ env }); const queryClient = useQueryClient(); - const { isPaidUser } = usePaidStatus(); const supportsMtls = resource.mode === "http"; - const isMtlsDisabled = !isPaidUser(tierMatrix.mtls); + const isMtlsDisabled = false; useEffect(() => { if (env.flags.disableEnterpriseFeatures || !supportsMtls) { @@ -206,7 +202,6 @@ export default function ResourceMtlsPage() { return ( <> -
Date: Mon, 19 Oct 2026 16:59:17 -0400 Subject: [PATCH 03/26] Update tier matrix --- server/lib/billing/tierMatrix.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts index d4aeb67f2..b4d0fb501 100644 --- a/server/lib/billing/tierMatrix.ts +++ b/server/lib/billing/tierMatrix.ts @@ -10,7 +10,6 @@ export enum TierFeature { ActionLogs = "actionLogs", // set the retention period to none on downgrade ConnectionLogs = "connectionLogs", AISessionLogs = "aiSessionLogs", - RotateCredentials = "rotateCredentials", MaintenancePage = "maintenancePage", // handle downgrade DevicePosture = "devicePosture", TwoFactorEnforcement = "twoFactorEnforcement", // handle downgrade by setting to optional @@ -26,8 +25,7 @@ export enum TierFeature { WildcardSubdomain = "wildcardSubdomain", NewtAutoUpdate = "newtAutoUpdate", ResourcePolicies = "resourcePolicies", - RoleBasedSSHControls = "roleBasedSSHControls", - Mtls = "mtls" // handle downgrade by disabling mTLS on resources + RoleBasedSSHControls = "roleBasedSSHControls" } export const tierMatrix: Record = { @@ -40,7 +38,6 @@ export const tierMatrix: Record = { [TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"], [TierFeature.ConnectionLogs]: ["tier2", "tier3", "enterprise"], [TierFeature.AISessionLogs]: ["tier2", "tier3", "enterprise"], - [TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.MaintenancePage]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"], [TierFeature.TwoFactorEnforcement]: [ @@ -71,6 +68,5 @@ export const tierMatrix: Record = { [TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.ResourcePolicies]: ["tier3", "enterprise"], - [TierFeature.RoleBasedSSHControls]: ["tier3", "enterprise"], - [TierFeature.Mtls]: ["tier3", "enterprise"] + [TierFeature.RoleBasedSSHControls]: ["tier3", "enterprise"] }; From bb5ba89e87033cc4e5c4e5a57d39f43241663171 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 19 Oct 2026 17:05:30 -0400 Subject: [PATCH 04/26] Move the paid feature alert down --- .../clients/machine/[niceId]/general/page.tsx | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/app/[orgId]/settings/clients/machine/[niceId]/general/page.tsx b/src/app/[orgId]/settings/clients/machine/[niceId]/general/page.tsx index 8bb9da553..aeb0cd34f 100644 --- a/src/app/[orgId]/settings/clients/machine/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/[niceId]/general/page.tsx @@ -209,10 +209,6 @@ export default function GeneralPage() { - {showRoles && ( - - )} -
{showRoles && ( - + <> + + + + )} From 702719ed76122d61ed665a2857309439b3a38f01 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 19 Oct 2026 17:15:22 -0400 Subject: [PATCH 05/26] Add badger to migration --- server/setup/scriptsPg/1.25.0.ts | 49 ++++++++++++++++++++++++++++ server/setup/scriptsSqlite/1.25.0.ts | 47 ++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/server/setup/scriptsPg/1.25.0.ts b/server/setup/scriptsPg/1.25.0.ts index 7e7d02e17..28c6e3563 100644 --- a/server/setup/scriptsPg/1.25.0.ts +++ b/server/setup/scriptsPg/1.25.0.ts @@ -1,5 +1,11 @@ import { db } from "@server/db/pg/driver"; +import { APP_PATH } from "@server/lib/consts"; import { sql } from "drizzle-orm"; +import fs from "fs"; +import * as yaml from "js-yaml"; +import path from "path"; +import z from "zod"; +import { fromZodError } from "zod-validation-error"; const version = "1.25.0"; @@ -95,5 +101,48 @@ export default async function migration() { throw e; } + try { + const traefikPath = path.join( + APP_PATH, + "traefik", + "traefik_config.yml" + ); + + const schema = z.object({ + experimental: z.object({ + plugins: z.object({ + badger: z.object({ + moduleName: z.string(), + version: z.string() + }) + }) + }) + }); + + const traefikFileContents = fs.readFileSync(traefikPath, "utf8"); + const traefikConfig = yaml.load(traefikFileContents) as any; + + const parsedConfig = schema.safeParse(traefikConfig); + + if (!parsedConfig.success) { + throw new Error(fromZodError(parsedConfig.error).toString()); + } + + traefikConfig.experimental.plugins.badger.version = "v1.8.0"; + + const updatedTraefikYaml = yaml.dump(traefikConfig); + + fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8"); + + console.log( + "Updated the version of Badger in your Traefik configuration to v1.8.0" + ); + } catch (e) { + console.log( + "We were unable to update the version of Badger in your Traefik configuration. Please update it manually. Check the release notes for this version for more information." + ); + console.error(e); + } + console.log(`${version} migration complete`); } diff --git a/server/setup/scriptsSqlite/1.25.0.ts b/server/setup/scriptsSqlite/1.25.0.ts index 79a8cc6c8..db6676554 100644 --- a/server/setup/scriptsSqlite/1.25.0.ts +++ b/server/setup/scriptsSqlite/1.25.0.ts @@ -1,6 +1,10 @@ import { APP_PATH } from "@server/lib/consts"; import Database from "better-sqlite3"; +import fs from "fs"; +import * as yaml from "js-yaml"; import path from "path"; +import z from "zod"; +import { fromZodError } from "zod-validation-error"; const version = "1.25.0"; @@ -104,5 +108,48 @@ export default async function migration() { throw e; } + try { + const traefikPath = path.join( + APP_PATH, + "traefik", + "traefik_config.yml" + ); + + const schema = z.object({ + experimental: z.object({ + plugins: z.object({ + badger: z.object({ + moduleName: z.string(), + version: z.string() + }) + }) + }) + }); + + const traefikFileContents = fs.readFileSync(traefikPath, "utf8"); + const traefikConfig = yaml.load(traefikFileContents) as any; + + const parsedConfig = schema.safeParse(traefikConfig); + + if (!parsedConfig.success) { + throw new Error(fromZodError(parsedConfig.error).toString()); + } + + traefikConfig.experimental.plugins.badger.version = "v1.8.0"; + + const updatedTraefikYaml = yaml.dump(traefikConfig); + + fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8"); + + console.log( + "Updated the version of Badger in your Traefik configuration to v1.8.0" + ); + } catch (e) { + console.log( + "We were unable to update the version of Badger in your Traefik configuration. Please update it manually. Check the release notes for this version for more information." + ); + console.error(e); + } + console.log(`${version} migration complete`); } From 77d149a96500bdc013017411c55f348957a95762 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 19 Oct 2026 17:24:51 -0400 Subject: [PATCH 06/26] Move mTLS into the auth config --- messages/en-US.json | 1 + .../public/[niceId]/authentication/page.tsx | 9 +- .../resources/public/[niceId]/layout.tsx | 7 - .../resources/public/[niceId]/mtls/page.tsx | 485 ------------------ .../ResourceMtlsCertificatesCredenza.tsx | 330 ++++++++++++ src/components/ResourceMtlsSection.tsx | 152 ++++++ 6 files changed, 491 insertions(+), 493 deletions(-) delete mode 100644 src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx create mode 100644 src/components/ResourceMtlsCertificatesCredenza.tsx create mode 100644 src/components/ResourceMtlsSection.tsx diff --git a/messages/en-US.json b/messages/en-US.json index a6aa0060f..6cb69d057 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -3956,6 +3956,7 @@ "mtlsCertificateErrorRemoveDescription": "The certificate could not be removed.", "mtlsLastCertificateRemoved": "mTLS was turned off because the last CA certificate was removed.", "mtlsNoCertificates": "No CA certificates added yet", + "mtlsSummary": "{count} trusted CA certificates", "mtlsRemoveCertificate": "Remove CA certificate", "mtlsExpired": "Expired", "mtlsExpiresSoon": "Expires soon", diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/authentication/page.tsx index 29c2f4825..ed04e2e1d 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/authentication/page.tsx @@ -1,7 +1,14 @@ "use client"; +import { ResourceMtlsSection } from "@app/components/ResourceMtlsSection"; import { ResourcePolicyEditForm } from "@app/components/resource-policy/ResourcePolicyEditForm"; +import { SettingsContainer } from "@app/components/Settings"; export default function ResourceAuthenticationPage() { - return ; + return ( + + + + + ); } diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx index 511fa6ac4..564abad66 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx @@ -114,13 +114,6 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { href: `/{orgId}/settings/resources/public/{niceId}/maintenance` }); } - - if (!env.flags.disableEnterpriseFeatures && resource.mode === "http") { - navItems.push({ - title: t("mtls"), - href: `/{orgId}/settings/resources/public/{niceId}/mtls` - }); - } } if (resource.mode === "inference") { diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx deleted file mode 100644 index 955eb041c..000000000 --- a/src/app/[orgId]/settings/resources/public/[niceId]/mtls/page.tsx +++ /dev/null @@ -1,485 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { Label } from "@/components/ui/label"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionFooter, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { Badge } from "@app/components/ui/badge"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useResourceContext } from "@app/hooks/useResourceContext"; -import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { resourceQueries } from "@app/lib/queries"; -import type { - CreateResourceMtlsCertificateResponse, - DeleteResourceMtlsCertificateResponse, - UpdateResourceResponse -} from "@server/routers/resource"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { AxiosResponse } from "axios"; -import { AlertCircle, FileKey2, Info, ShieldCheck, Trash2 } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { useParams, useRouter } from "next/navigation"; -import { useEffect, useRef, useState } from "react"; - -const DAY_MS = 24 * 60 * 60 * 1000; -const EXPIRES_SOON_MS = 30 * DAY_MS; - -export default function ResourceMtlsPage() { - const params = useParams(); - const router = useRouter(); - const { env } = useEnvContext(); - const { resource, updateResource } = useResourceContext(); - const t = useTranslations(); - const api = createApiClient({ env }); - const queryClient = useQueryClient(); - - const supportsMtls = resource.mode === "http"; - const isMtlsDisabled = false; - - useEffect(() => { - if (env.flags.disableEnterpriseFeatures || !supportsMtls) { - router.replace( - `/${params.orgId}/settings/resources/public/${resource.niceId}/general` - ); - } - }, [ - env.flags.disableEnterpriseFeatures, - params.orgId, - resource.niceId, - router, - supportsMtls - ]); - - const { data: certificates = [], isLoading: isLoadingCertificates } = - useQuery( - resourceQueries.resourceMtlsCertificates({ - resourceId: resource.resourceId - }) - ); - - const [name, setName] = useState(""); - const [pem, setPem] = useState(""); - const [adding, setAdding] = useState(false); - const [removingId, setRemovingId] = useState(null); - const fileInputRef = useRef(null); - - const [mtlsEnabled, setMtlsEnabled] = useState(resource.mtlsEnabled); - const [saving, setSaving] = useState(false); - - // The server switches mTLS off when the last CA is removed. - useEffect(() => { - setMtlsEnabled(resource.mtlsEnabled); - }, [resource.mtlsEnabled]); - - if (env.flags.disableEnterpriseFeatures || !supportsMtls) { - return null; - } - - const hasCertificates = certificates.length > 0; - const canEnable = hasCertificates && resource.ssl; - - async function invalidateCertificates() { - await queryClient.invalidateQueries( - resourceQueries.resourceMtlsCertificates({ - resourceId: resource.resourceId - }) - ); - } - - async function onFileSelected(e: React.ChangeEvent) { - const file = e.target.files?.[0]; - e.target.value = ""; - if (!file) return; - const text = await file.text(); - setPem((prev) => (prev.trim() ? `${prev.trim()}\n${text}` : text)); - if (!name && certificates.length === 0) { - setName(file.name.replace(/\.(pem|crt|cer|ca-bundle)$/i, "")); - } - } - - async function onAddCertificate() { - if (!pem.trim()) return; - setAdding(true); - try { - await api.put>( - `/resource/${resource.resourceId}/mtls/certificate`, - { - certificate: pem, - ...(name.trim() ? { name: name.trim() } : {}) - } - ); - setPem(""); - setName(""); - await invalidateCertificates(); - toast({ - title: t("mtlsCertificateAdded"), - description: t("mtlsCertificateAddedDescription") - }); - } catch (e) { - toast({ - variant: "destructive", - title: t("mtlsCertificateErrorAdd"), - description: formatAxiosError( - e, - t("mtlsCertificateErrorAddDescription") - ) - }); - } finally { - setAdding(false); - } - } - - async function onRemoveCertificate(mtlsCertificateId: number) { - setRemovingId(mtlsCertificateId); - try { - const res = await api.delete< - AxiosResponse - >( - `/resource/${resource.resourceId}/mtls/certificate/${mtlsCertificateId}` - ); - await invalidateCertificates(); - - if (res.data.data.mtlsDisabled) { - updateResource({ mtlsEnabled: false }); - toast({ - title: t("mtlsCertificateRemoved"), - description: t("mtlsLastCertificateRemoved") - }); - } else { - toast({ title: t("mtlsCertificateRemoved") }); - } - } catch (e) { - toast({ - variant: "destructive", - title: t("mtlsCertificateErrorRemove"), - description: formatAxiosError( - e, - t("mtlsCertificateErrorRemoveDescription") - ) - }); - } finally { - setRemovingId(null); - } - } - - async function onSaveEnforcement() { - setSaving(true); - try { - await api.post>( - `/resource/${resource.resourceId}`, - { mtlsEnabled } - ); - updateResource({ mtlsEnabled }); - toast({ - title: t("resourceUpdated"), - description: t("resourceUpdatedDescription") - }); - } catch (e) { - toast({ - variant: "destructive", - title: t("resourceErrorUpdate"), - description: formatAxiosError( - e, - t("resourceErrorUpdateDescription") - ) - }); - } finally { - setSaving(false); - } - } - - return ( - <> -
- - - - - {t("mtlsCaCertificates")} - - - {t("mtlsCaCertificatesDescription")} - - - - -
- {isLoadingCertificates ? null : hasCertificates ? ( -
    - {certificates.map((cert) => { - const now = Date.now(); - const expired = - cert.notAfter != null && - cert.notAfter < now; - const expiresSoon = - !expired && - cert.notAfter != null && - cert.notAfter - now < - EXPIRES_SOON_MS; - return ( -
  • -
    - -
    -
    - - {cert.name || - cert.subject || - t( - "mtlsCaCertificates" - )} - - {expired && ( - - {t( - "mtlsExpired" - )} - - )} - {expiresSoon && ( - - {t( - "mtlsExpiresSoon" - )} - - )} -
    - {cert.name && - cert.subject && ( -

    - { - cert.subject - } -

    - )} - {cert.issuer && ( -

    - {t( - "mtlsIssuedBy", - { - issuer: cert.issuer - } - )} -

    - )} - {cert.notAfter != - null && ( -

    - {t( - "mtlsExpiresOn", - { - date: new Date( - cert.notAfter - ).toLocaleDateString() - } - )} -

    - )} - {cert.fingerprint && ( -

    - { - cert.fingerprint - } -

    - )} -
    -
    - -
  • - ); - })} -
- ) : ( -

- {t("mtlsNoCertificates")} -

- )} - -
-
- - - setName(e.target.value) - } - /> -
-
- -