diff --git a/messages/en-US.json b/messages/en-US.json index 788e7e63..fa51555a 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2221,7 +2221,7 @@ "credentials": "Credentials", "savecredentials": "Save Credentials", "regenerateCredentialsButton": "Regenerate Credentials", - "regenerateCredentials": "Regenerate and save your credentials", + "regenerateCredentials": "Regenerate 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.", @@ -2253,5 +2253,20 @@ "clientAddress": "Client Address (Advanced)", "setupFailedToFetchSubnet": "Failed to fetch default subnet", "setupSubnetAdvanced": "Subnet (Advanced)", - "setupSubnetDescription": "The subnet for this organization's internal network." + "setupSubnetDescription": "The subnet for this organization's internal network.", + "siteRegenerateAndDisconnect": "Regenerate and Disconnect", + "siteRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this site?", + "siteRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the site. The site will need to be restarted with the new credentials.", + "siteRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this site?", + "siteRegenerateCredentialsWarning": "This will regenerate the credentials. The site will stay connected until you manually restart it and use the new credentials.", + "clientRegenerateAndDisconnect": "Regenerate and Disconnect", + "clientRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this client?", + "clientRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the client. The client will need to be restarted with the new credentials.", + "clientRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this client?", + "clientRegenerateCredentialsWarning": "This will regenerate the credentials. The client will stay connected until you manually restart it and use the new credentials.", + "remoteExitNodeRegenerateAndDisconnect": "Regenerate and Disconnect", + "remoteExitNodeRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this remote exit node?", + "remoteExitNodeRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the remote exit node. The remote exit node will need to be restarted with the new credentials.", + "remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?", + "remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials." } diff --git a/server/private/routers/re-key/reGenerateClientSecret.ts b/server/private/routers/re-key/reGenerateClientSecret.ts index 67e22b63..310f2602 100644 --- a/server/private/routers/re-key/reGenerateClientSecret.ts +++ b/server/private/routers/re-key/reGenerateClientSecret.ts @@ -31,7 +31,8 @@ const reGenerateSecretParamsSchema = z.strictObject({ const reGenerateSecretBodySchema = z.strictObject({ // olmId: z.string().min(1).optional(), - secret: z.string().min(1) + secret: z.string().min(1), + disconnect: z.boolean().optional().default(true) }); export type ReGenerateSecretBody = z.infer; @@ -52,7 +53,7 @@ export async function reGenerateClientSecret( ); } - const { secret } = parsedBody.data; + const { secret, disconnect } = parsedBody.data; const parsedParams = reGenerateSecretParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -114,18 +115,21 @@ export async function reGenerateClientSecret( }) .where(eq(olms.olmId, existingOlms[0].olmId)); - const payload = { - type: `olm/terminate`, - data: {} - }; - // Don't await this to prevent blocking the response - sendToClient(existingOlms[0].olmId, payload).catch((error) => { - logger.error("Failed to send termination message to olm:", error); - }); + // Only disconnect if explicitly requested + if (disconnect) { + const payload = { + type: `olm/terminate`, + data: {} + }; + // Don't await this to prevent blocking the response + sendToClient(existingOlms[0].olmId, payload).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); - }); + disconnectClient(existingOlms[0].olmId).catch((error) => { + logger.error("Failed to disconnect olm after re-key:", error); + }); + } return response(res, { data: { diff --git a/server/private/routers/re-key/reGenerateExitNodeSecret.ts b/server/private/routers/re-key/reGenerateExitNodeSecret.ts index ed2fb7d5..b642f102 100644 --- a/server/private/routers/re-key/reGenerateExitNodeSecret.ts +++ b/server/private/routers/re-key/reGenerateExitNodeSecret.ts @@ -23,7 +23,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 } from "#private/routers/ws"; +import { disconnectClient, sendToClient } from "#private/routers/ws"; export const paramsSchema = z.object({ orgId: z.string() @@ -31,7 +31,8 @@ export const paramsSchema = z.object({ const bodySchema = z.strictObject({ remoteExitNodeId: z.string().length(15), - secret: z.string().length(48) + secret: z.string().length(48), + disconnect: z.boolean().optional().default(true) }); export async function reGenerateExitNodeSecret( @@ -60,7 +61,7 @@ export async function reGenerateExitNodeSecret( ); } - const { remoteExitNodeId, secret } = parsedBody.data; + const { remoteExitNodeId, secret, disconnect } = parsedBody.data; const [existingRemoteExitNode] = await db .select() @@ -83,11 +84,31 @@ export async function reGenerateExitNodeSecret( .set({ secretHash }) .where(eq(remoteExitNodes.remoteExitNodeId, remoteExitNodeId)); - disconnectClient(existingRemoteExitNode.remoteExitNodeId).catch( - (error) => { - logger.error("Failed to disconnect newt after re-key:", error); - } - ); + // 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, diff --git a/server/private/routers/re-key/reGenerateSiteSecret.ts b/server/private/routers/re-key/reGenerateSiteSecret.ts index 62fb286d..b427dcc2 100644 --- a/server/private/routers/re-key/reGenerateSiteSecret.ts +++ b/server/private/routers/re-key/reGenerateSiteSecret.ts @@ -33,7 +33,8 @@ const updateSiteParamsSchema = z.strictObject({ const updateSiteBodySchema = z.strictObject({ type: z.enum(["newt", "wireguard"]), secret: z.string().min(1).max(255).optional(), - pubKey: z.string().optional() + pubKey: z.string().optional(), + disconnect: z.boolean().optional().default(true) }); export async function reGenerateSiteSecret( @@ -63,7 +64,7 @@ export async function reGenerateSiteSecret( } const { siteId } = parsedParams.data; - const { type, pubKey, secret } = parsedBody.data; + const { type, pubKey, secret, disconnect } = parsedBody.data; let existingNewt: Newt | null = null; if (type === "newt") { @@ -112,21 +113,24 @@ export async function reGenerateSiteSecret( }) .where(eq(newts.newtId, existingNewts[0].newtId)); - 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 - ); - }); + // 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); - }); + 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") { 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 9c416034..e5cd5d4f 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 @@ -55,6 +55,7 @@ export default function CredentialsPage() { null ); const [showCredentialsAlert, setShowCredentialsAlert] = useState(false); + const [shouldDisconnect, setShouldDisconnect] = useState(true); const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const subscription = useSubscriptionStatusContext(); @@ -79,7 +80,8 @@ export default function CredentialsPage() { AxiosResponse >(`/re-key/${orgId}/regenerate-remote-exit-node-secret`, { remoteExitNodeId: remoteExitNode.remoteExitNodeId, - secret: data.secret + secret: data.secret, + disconnect: shouldDisconnect }); if (rekeyRes && rekeyRes.status === 200) { @@ -193,12 +195,27 @@ export default function CredentialsPage() { )} - +
+ + +
@@ -216,11 +233,32 @@ export default function CredentialsPage() { }} dialog={
-

{t("regenerateCredentialsConfirmation")}

-

{t("regenerateCredentialsWarning")}

+ {shouldDisconnect ? ( + <> +

+ {t("remoteExitNodeRegenerateAndDisconnectConfirmation")} +

+

+ {t("remoteExitNodeRegenerateAndDisconnectWarning")} +

+ + ) : ( + <> +

+ {t("remoteExitNodeRegenerateCredentialsConfirmation")} +

+

+ {t("remoteExitNodeRegenerateCredentialsWarning")} +

+ + )}
} - buttonText={t("regenerateCredentialsButton")} + buttonText={ + shouldDisconnect + ? t("remoteExitNodeRegenerateAndDisconnect") + : t("regenerateCredentialsButton") + } onConfirm={handleConfirmRegenerate} string={getConfirmationString()} title={t("regenerateCredentials")} diff --git a/src/app/[orgId]/settings/clients/machine/[clientId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/machine/[clientId]/credentials/page.tsx index 342cb9e8..2cdaf906 100644 --- a/src/app/[orgId]/settings/clients/machine/[clientId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/[clientId]/credentials/page.tsx @@ -49,6 +49,7 @@ export default function CredentialsPage() { null ); const [showCredentialsAlert, setShowCredentialsAlert] = useState(false); + const [shouldDisconnect, setShouldDisconnect] = useState(true); const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const subscription = useSubscriptionStatusContext(); @@ -69,7 +70,8 @@ export default function CredentialsPage() { const rekeyRes = await api.post( `/re-key/${client?.clientId}/regenerate-client-secret`, { - secret: data.olmSecret + secret: data.olmSecret, + disconnect: shouldDisconnect } ); @@ -173,12 +175,27 @@ export default function CredentialsPage() { )} - +
+ + +
@@ -196,11 +213,32 @@ export default function CredentialsPage() { }} dialog={
-

{t("regenerateCredentialsConfirmation")}

-

{t("regenerateCredentialsWarning")}

+ {shouldDisconnect ? ( + <> +

+ {t("clientRegenerateAndDisconnectConfirmation")} +

+

+ {t("clientRegenerateAndDisconnectWarning")} +

+ + ) : ( + <> +

+ {t("clientRegenerateCredentialsConfirmation")} +

+

+ {t("clientRegenerateCredentialsWarning")} +

+ + )}
} - buttonText={t("regenerateCredentialsButton")} + buttonText={ + shouldDisconnect + ? t("clientRegenerateAndDisconnect") + : t("regenerateCredentialsButton") + } onConfirm={handleConfirmRegenerate} string={getConfirmationString()} title={t("regenerateCredentials")} diff --git a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx index 15c071d2..c27caadb 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx @@ -53,13 +53,16 @@ export default function CredentialsPage() { useState(null); const [wgConfig, setWgConfig] = useState(""); const [publicKey, setPublicKey] = useState(""); - const [currentNewtId, setCurrentNewtId] = useState(site.newtId); + const [currentNewtId, setCurrentNewtId] = useState( + site.newtId + ); const [regeneratedSecret, setRegeneratedSecret] = useState( null ); const [showCredentialsAlert, setShowCredentialsAlert] = useState(false); const [showWireGuardAlert, setShowWireGuardAlert] = useState(false); const [loadingDefaults, setLoadingDefaults] = useState(false); + const [shouldDisconnect, setShouldDisconnect] = useState(true); const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const subscription = useSubscriptionStatusContext(); @@ -77,7 +80,9 @@ export default function CredentialsPage() { if (site?.type === "wireguard" && !siteDefaults && orgId) { setLoadingDefaults(true); try { - const res = await api.get(`/org/${orgId}/pick-site-defaults`); + const res = await api.get( + `/org/${orgId}/pick-site-defaults` + ); if (res && res.status === 200) { setSiteDefaults(res.data.data); } @@ -93,7 +98,6 @@ export default function CredentialsPage() { fetchSiteDefaults(); }, []); - const handleConfirmRegenerate = async () => { try { let generatedPublicKey = ""; @@ -140,7 +144,8 @@ export default function CredentialsPage() { `/re-key/${site?.siteId}/regenerate-site-secret`, { type: "newt", - secret: data.newtSecret + secret: data.newtSecret, + disconnect: shouldDisconnect } ); @@ -233,7 +238,11 @@ export default function CredentialsPage() { text={displaySecret} /> ) : ( - {"••••••••••••••••••••••••••••••••"} + + { + "••••••••••••••••••••••••••••••••" + } + )} @@ -252,12 +261,27 @@ export default function CredentialsPage() { )} - +
+ + +
)} @@ -280,7 +304,10 @@ export default function CredentialsPage() { <> {wgConfig ? (
- +
) : ( )} {showWireGuardAlert && wgConfig && ( - + {t("siteCredentialsSave")} - {t("siteCredentialsSaveDescription")} + {t( + "siteCredentialsSaveDescription" + )} )} @@ -322,7 +372,7 @@ export default function CredentialsPage() { onClick={() => setModalOpen(true)} disabled={isSecurityFeatureDisabled()} > - {t("regenerateCredentialsButton")} + {t("siteRegenerateAndDisconnect")} @@ -343,11 +393,38 @@ export default function CredentialsPage() { }} dialog={
-

{t("regenerateCredentialsConfirmation")}

-

{t("regenerateCredentialsWarning")}

+ {shouldDisconnect ? ( + <> +

+ {t( + "siteRegenerateAndDisconnectConfirmation" + )} +

+

+ {t( + "siteRegenerateAndDisconnectWarning" + )} +

+ + ) : ( + <> +

+ {t( + "siteRegenerateCredentialsConfirmation" + )} +

+

+ {t("siteRegenerateCredentialsWarning")} +

+ + )}
} - buttonText={t("regenerateCredentialsButton")} + buttonText={ + shouldDisconnect + ? t("siteRegenerateAndDisconnect") + : t("regenerateCredentialsButton") + } onConfirm={handleConfirmRegenerate} string={getConfirmationString()} title={t("regenerateCredentials")}