Skip to content
Open
13 changes: 12 additions & 1 deletion messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2081,5 +2081,16 @@
"supportSend": "Send",
"supportMessageSent": "Message Sent!",
"supportWillContact": "We'll be in touch shortly!",
"selectLogRetention": "Select log retention"
"selectLogRetention": "Select log retention",
"credentials": "Credentials",
"savecredentials": "Save Credentials",
"regeneratecredentials": "Regenerate Credentials",
"regenerateCredentials": "Regenerate and save your credentials",
"generatedcredentials": "Generated Credentials",
"copyandsavethesecredentials": "Copy and save these credentials",
"copyandsavethesecredentialsdescription": "These credentials will not be shown again after you leave this page. Save them securely now.",
"credentialsSaved" : "Credentials Saved",
"credentialsSavedDescription": "Credentials have been regenerated and saved successfully.",
"credentialsSaveError": "Credentials Save Error",
"credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials."
}
1 change: 1 addition & 0 deletions server/auth/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum ActionsEnum {
getSite = "getSite",
listSites = "listSites",
updateSite = "updateSite",
reGenerateSecret = "reGenerateSecret",
createResource = "createResource",
deleteResource = "deleteResource",
getResource = "getResource",
Expand Down
8 changes: 8 additions & 0 deletions server/private/routers/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,14 @@ authenticated.put(
remoteExitNode.createRemoteExitNode
);

authenticated.put(
"/org/:orgId/reGenerate-remote-exit-node-secret",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.updateRemoteExitNode),
remoteExitNode.reGenerateExitNodeSecret
);

authenticated.get(
"/org/:orgId/remote-exit-nodes",
verifyValidLicense,
Expand Down
1 change: 1 addition & 0 deletions server/private/routers/remoteExitNode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from "./deleteRemoteExitNode";
export * from "./listRemoteExitNodes";
export * from "./pickRemoteExitNodeDefaults";
export * from "./quickStartRemoteExitNode";
export * from "./reGenerateExitNodeSecret";
106 changes: 106 additions & 0 deletions server/private/routers/remoteExitNode/reGenerateExitNodeSecret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 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 } 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 { UpdateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
import { paramsSchema } from "./createRemoteExitNode";

const bodySchema = z
.object({
remoteExitNodeId: z.string().length(15),
secret: z.string().length(48)
})
.strict();

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 } = parsedBody.data;

if (req.user && !req.userOrgRoleId) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);
}

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));

return response<UpdateRemoteExitNodeResponse>(res, {
data: {
remoteExitNodeId,
secret,
},
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"
)
);
}
}
3 changes: 2 additions & 1 deletion server/routers/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from "./createClient";
export * from "./deleteClient";
export * from "./listClients";
export * from "./updateClient";
export * from "./getClient";
export * from "./getClient";
export * from "./reGenerateClientSecret";
130 changes: 130 additions & 0 deletions server/routers/client/reGenerateClientSecret.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, 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";

const reGenerateSecretParamsSchema = z
.object({
clientId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();

const reGenerateSecretBodySchema = z
.object({
olmId: z.string().min(1).optional(),
secret: z.string().min(1).optional(),

})
.strict();

export type ReGenerateSecretBody = z.infer<typeof reGenerateSecretBodySchema>;

registry.registerPath({
method: "post",
path: "/client/{clientId}/regenerate-secret",
description: "Regenerate a client's OLM credentials by its client ID.",
tags: [OpenAPITags.Client],
request: {
params: reGenerateSecretParamsSchema,
body: {
content: {
"application/json": {
schema: reGenerateSecretBodySchema
}
}
}
},
responses: {}
});


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 { olmId, secret } = 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;

let secretHash = undefined;
if (secret) {
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 [existingOlm] = await db
.select()
.from(olms)
.where(eq(olms.clientId, clientId))
.limit(1);

if (existingOlm && olmId && secretHash) {
await db
.update(olms)
.set({
olmId,
secretHash
})
.where(eq(olms.clientId, clientId));
}

return response(res, {
data: existingOlm,
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")
);
}
}
6 changes: 4 additions & 2 deletions server/routers/client/updateClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { Client, db, exitNodes, sites } from "@server/db";
import { Client, db, exitNodes, olms, sites } from "@server/db";
import { clients, clientSites } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
Expand All @@ -18,6 +18,7 @@ import {
deletePeer as olmDeletePeer
} from "../olm/peers";
import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { hashPassword } from "@server/auth/password";

const updateClientParamsSchema = z
.object({
Expand All @@ -30,7 +31,7 @@ const updateClientSchema = z
name: z.string().min(1).max(255).optional(),
siteIds: z
.array(z.number().int().positive())
.optional()
.optional(),
})
.strict();

Expand Down Expand Up @@ -89,6 +90,7 @@ export async function updateClient(

const { clientId } = parsedParams.data;


// Fetch the client to make sure it exists and the user has access to it
const [client] = await db
.select()
Expand Down
15 changes: 15 additions & 0 deletions server/routers/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ authenticated.post(
client.updateClient,
);

authenticated.post(
"/client/:clientId/regenerate-secret",
verifyClientsEnabled,
verifyClientAccess,
verifyUserHasAction(ActionsEnum.reGenerateSecret),
client.reGenerateClientSecret
);

// authenticated.get(
// "/site/:siteId/roles",
// verifySiteAccess,
Expand All @@ -191,6 +199,13 @@ authenticated.post(
logActionAudit(ActionsEnum.updateSite),
site.updateSite,
);

authenticated.post(
"/site/:siteId/regenerate-secret",
verifySiteAccess,
verifyUserHasAction(ActionsEnum.reGenerateSecret),
site.reGenerateSiteSecret
);
authenticated.delete(
"/site/:siteId",
verifySiteAccess,
Expand Down
5 changes: 5 additions & 0 deletions server/routers/remoteExitNode/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export type CreateRemoteExitNodeResponse = {
secret: string;
};

export type UpdateRemoteExitNodeResponse = {
remoteExitNodeId: string;
secret: string;
}

export type PickRemoteExitNodeDefaultsResponse = {
remoteExitNodeId: string;
secret: string;
Expand Down
1 change: 1 addition & 0 deletions server/routers/site/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from "./listSites";
export * from "./listSiteRoles";
export * from "./pickSiteDefaults";
export * from "./socketIntegration";
export * from "./reGenerateSiteSecret";
Loading
Loading