Make rekey not private and in integration api

This commit is contained in:
Owen
2026-10-19 16:55:10 -04:00
parent 4dcae5ff58
commit ab4c105e75
18 changed files with 221 additions and 165 deletions
+1
View File
@@ -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",
-11
View File
@@ -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");
+2 -1
View File
@@ -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
-31
View File
@@ -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,
-16
View File
@@ -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";
@@ -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";
@@ -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";
@@ -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";
+25
View File
@@ -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);
+28
View File
@@ -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,
+3
View File
@@ -0,0 +1,3 @@
export * from "./reGenerateClientSecret";
export * from "./reGenerateSiteSecret";
export * from "./reGenerateExitNodeSecret";
@@ -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<typeof reGenerateSecretBodySchema>;
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,
@@ -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(
@@ -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,
@@ -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<PickRemoteExitNodeDefaultsResponse | null>(null);
@@ -129,10 +124,6 @@ export default function CredentialsPage() {
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<PaidFeaturesAlert
tiers={tierMatrix.rotateCredentials}
/>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
@@ -196,9 +187,6 @@ export default function CredentialsPage() {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={
!isPaidUser(tierMatrix.rotateCredentials)
}
>
{t("regenerateCredentialsButton")}
</Button>
@@ -207,9 +195,6 @@ export default function CredentialsPage() {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={
!isPaidUser(tierMatrix.rotateCredentials)
}
>
{t("remoteExitNodeRegenerateAndDisconnect")}
</Button>
@@ -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() {
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<PaidFeaturesAlert
tiers={tierMatrix.rotateCredentials}
/>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
@@ -182,9 +173,6 @@ export default function CredentialsPage() {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={
!isPaidUser(tierMatrix.rotateCredentials)
}
>
{t("regenerateCredentialsButton")}
</Button>
@@ -193,9 +181,6 @@ export default function CredentialsPage() {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={
!isPaidUser(tierMatrix.rotateCredentials)
}
>
{t("clientRegenerateAndDisconnect")}
</Button>
@@ -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() {
</SettingsSectionDescription>
</SettingsSectionHeader>
<PaidFeaturesAlert
tiers={tierMatrix.rotateCredentials}
/>
<SettingsSectionBody>
<InfoSections cols={3}>
<InfoSection>
@@ -280,11 +271,6 @@ export default function CredentialsPage() {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={
!isPaidUser(
tierMatrix.rotateCredentials
)
}
>
{t("regenerateCredentialsButton")}
</Button>
@@ -293,11 +279,6 @@ export default function CredentialsPage() {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={
!isPaidUser(
tierMatrix.rotateCredentials
)
}
>
{t("siteRegenerateAndDisconnect")}
</Button>
@@ -324,10 +305,6 @@ export default function CredentialsPage() {
</SettingsSectionDescription>
</SettingsSectionHeader>
<PaidFeaturesAlert
tiers={tierMatrix.rotateCredentials}
/>
<SettingsSectionBody>
{!loadingDefaults && (
<>
@@ -393,14 +370,7 @@ export default function CredentialsPage() {
</SettingsSectionBody>
{!env.flags.disableEnterpriseFeatures && (
<SettingsSectionFooter>
<Button
onClick={() => setModalOpen(true)}
disabled={
!isPaidUser(
tierMatrix.rotateCredentials
)
}
>
<Button onClick={() => setModalOpen(true)}>
{t("siteRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
+2 -1
View File
@@ -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: {