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
-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,154 +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.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, Olm, olms } from "@server/db";
import { clients } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
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 { OlmErrorCodes, sendOlmError } from "@server/routers/olm/error";
import { sendTerminateClient } from "@server/routers/client/terminate";
const reGenerateSecretParamsSchema = z.strictObject({
clientId: z.coerce.number().int().positive()
});
const reGenerateSecretBodySchema = z.strictObject({
// olmId: z.string().min(1).optional(),
secret: z.string().min(1),
disconnect: z.boolean().optional().default(true)
});
export type ReGenerateSecretBody = z.infer<typeof reGenerateSecretBodySchema>;
export async function reGenerateClientSecret(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = reGenerateSecretBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { secret, disconnect } = parsedBody.data;
const parsedParams = reGenerateSecretParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { clientId } = parsedParams.data;
const secretHash = await hashPassword(secret);
// Fetch the client to make sure it exists and the user has access to it
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with ID ${clientId} not found`
)
);
}
const existingOlms = await db
.select()
.from(olms)
.where(eq(olms.clientId, clientId));
if (existingOlms.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`No OLM found for client ID ${clientId}`
)
);
}
if (existingOlms.length > 1) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Multiple OLM entries found for client ID ${clientId}`
)
);
}
await db
.update(olms)
.set({
secretHash
})
.where(eq(olms.olmId, existingOlms[0].olmId));
// Only disconnect if explicitly requested
if (disconnect) {
// Don't await this to prevent blocking the response
sendTerminateClient(
clientId,
OlmErrorCodes.TERMINATED_REKEYED,
existingOlms[0].olmId
).catch((error) => {
logger.error(
"Failed to send termination message to olm:",
error
);
});
disconnectClient(existingOlms[0].olmId).catch((error) => {
logger.error("Failed to disconnect olm after re-key:", error);
});
}
return response(res, {
data: {
olmId: existingOlms[0].olmId
},
success: true,
error: false,
message: "Credentials regenerated successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -1,137 +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.
*/
import { NextFunction, Request, Response } from "express";
import {
db,
exitNodes,
exitNodeOrgs,
ExitNode,
ExitNodeOrg,
RemoteExitNode
} from "@server/db";
import HttpCode from "@server/types/HttpCode";
import { z } from "zod";
import { remoteExitNodes } from "@server/db";
import createHttpError from "http-errors";
import response from "@server/lib/response";
import { fromError } from "zod-validation-error";
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";
export const paramsSchema = z.object({
orgId: z.string()
});
const bodySchema = z.strictObject({
remoteExitNodeId: z.string().length(15),
secret: z.string().length(48),
disconnect: z.boolean().optional().default(true)
});
export async function reGenerateExitNodeSecret(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { remoteExitNodeId, secret, disconnect } = parsedBody.data;
const [existingRemoteExitNode] = await db
.select()
.from(remoteExitNodes)
.where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId));
if (!existingRemoteExitNode) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Remote Exit Node does not exist"
)
);
}
const secretHash = await hashPassword(secret);
await db
.update(remoteExitNodes)
.set({ secretHash })
.where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId));
// Only disconnect if explicitly requested
if (disconnect) {
const payload = {
type: `remoteExitNode/terminate`,
data: {}
};
// Don't await this to prevent blocking the response
sendToClient(
existingRemoteExitNode.remoteExitNodeId,
payload
).catch((error) => {
logger.error(
"Failed to send termination message to remote exit node:",
error
);
});
disconnectClient(existingRemoteExitNode.remoteExitNodeId).catch(
(error) => {
logger.error(
"Failed to disconnect remote exit node after re-key:",
error
);
}
);
}
return response(res, {
data: null,
success: true,
error: false,
message: "Remote Exit Node secret updated successfully",
status: HttpCode.OK
});
} catch (e) {
logger.error("Failed to update remoteExitNode", e);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to update remoteExitNode"
)
);
}
}
@@ -1,224 +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.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, Newt, newts, sites } from "@server/db";
import { 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";
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";
const updateSiteParamsSchema = z.strictObject({
siteId: z.coerce.number().int().positive()
});
const updateSiteBodySchema = z.strictObject({
type: z.enum(["newt", "wireguard"]),
secret: z.string().min(1).max(255).optional(),
pubKey: z.string().optional(),
disconnect: z.boolean().optional().default(true)
});
export async function reGenerateSiteSecret(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = updateSiteParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = updateSiteBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { siteId } = parsedParams.data;
const { type, pubKey, secret, disconnect } = parsedBody.data;
let existingNewt: Newt | null = null;
if (type === "newt") {
if (!secret) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"newtSecret is required for newt sites"
)
);
}
const secretHash = await hashPassword(secret);
// get the newt to verify it exists
const existingNewts = await db
.select()
.from(newts)
.where(eq(newts.siteId, siteId));
if (existingNewts.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`No Newt found for site ID ${siteId}`
)
);
}
if (existingNewts.length > 1) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Multiple Newts found for site ID ${siteId}`
)
);
}
existingNewt = existingNewts[0];
// update the secret on the existing newt
await db
.update(newts)
.set({
secretHash
})
.where(eq(newts.newtId, existingNewts[0].newtId));
// Only disconnect if explicitly requested
if (disconnect) {
const payload = {
type: `newt/wg/terminate`,
data: {}
};
// Don't await this to prevent blocking the response
sendToClient(existingNewts[0].newtId, payload).catch(
(error) => {
logger.error(
"Failed to send termination message to newt:",
error
);
}
);
disconnectClient(existingNewts[0].newtId).catch((error) => {
logger.error(
"Failed to disconnect newt after re-key:",
error
);
});
}
logger.info(`Regenerated Newt credentials for site ${siteId}`);
} else if (type === "wireguard") {
if (!pubKey) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Public key is required for wireguard sites"
)
);
}
try {
const [site] = await db
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.limit(1);
if (!site) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${siteId} not found`
)
);
}
await db
.update(sites)
.set({ pubKey })
.where(eq(sites.siteId, siteId));
if (!site) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site with ID ${siteId} not found`
)
);
}
if (site.exitNodeId && site.exitNodeSubnet) {
await deletePeer(site.exitNodeId, site.pubKey!); // the old pubkey
await addPeer(site.exitNodeId, {
publicKey: pubKey,
allowedIps: await getAllowedIps(site.siteId)
});
}
logger.info(
`Regenerated WireGuard credentials for site ${siteId}`
);
} catch (err) {
logger.error(
`Transaction failed while regenerating WireGuard secret for site ${siteId}`,
err
);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to regenerate WireGuard credentials. Rolled back transaction."
)
);
}
}
return response(res, {
data: {
newtId: existingNewt ? existingNewt.newtId : undefined
},
success: true,
error: false,
message: "Credentials regenerated successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error("Unexpected error in reGenerateSiteSecret", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"An unexpected error occurred"
)
);
}
}
@@ -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";