diff --git a/messages/en-US.json b/messages/en-US.json index f8a1f973..788e7e63 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2053,7 +2053,7 @@ "pathRewriteStripLabel": "strip", "sidebarEnableEnterpriseLicense": "Enable Enterprise License", "cannotbeUndone": "This can not be undone.", - "toConfirm": "to confirm", + "toConfirm": "to confirm.", "deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?", "clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.", "sidebarLogs": "Logs", @@ -2220,7 +2220,7 @@ "regenerate": "Regenerate", "credentials": "Credentials", "savecredentials": "Save Credentials", - "regeneratecredentials": "Re-key", + "regenerateCredentialsButton": "Regenerate Credentials", "regenerateCredentials": "Regenerate and save your credentials", "generatedcredentials": "Generated Credentials", "copyandsavethesecredentials": "Copy and save these credentials", @@ -2229,7 +2229,7 @@ "credentialsSavedDescription": "Credentials have been regenerated and saved successfully.", "credentialsSaveError": "Credentials Save Error", "credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials.", - "regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones. Make sure to update any configurations that use these credentials.", + "regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones and cause a disconnection. Make sure to update any configurations that use these credentials.", "confirm": "Confirm", "regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?", "endpoint": "Endpoint", diff --git a/server/private/routers/re-key/reGenerateClientSecret.ts b/server/private/routers/re-key/reGenerateClientSecret.ts index 870d76c2..67e22b63 100644 --- a/server/private/routers/re-key/reGenerateClientSecret.ts +++ b/server/private/routers/re-key/reGenerateClientSecret.ts @@ -36,24 +36,6 @@ const reGenerateSecretBodySchema = z.strictObject({ export type ReGenerateSecretBody = z.infer; -registry.registerPath({ - method: "post", - path: "/re-key/{clientId}/regenerate-client-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, diff --git a/server/private/routers/re-key/reGenerateExitNodeSecret.ts b/server/private/routers/re-key/reGenerateExitNodeSecret.ts index 0ef5b9ce..ed2fb7d5 100644 --- a/server/private/routers/re-key/reGenerateExitNodeSecret.ts +++ b/server/private/routers/re-key/reGenerateExitNodeSecret.ts @@ -34,24 +34,6 @@ const bodySchema = z.strictObject({ secret: z.string().length(48) }); -registry.registerPath({ - method: "post", - path: "/re-key/{orgId}/regenerate-secret", - description: "Regenerate a exit node credentials by its org ID.", - tags: [OpenAPITags.Org], - request: { - params: paramsSchema, - body: { - content: { - "application/json": { - schema: bodySchema - } - } - } - }, - responses: {} -}); - export async function reGenerateExitNodeSecret( req: Request, res: Response, @@ -108,7 +90,7 @@ export async function reGenerateExitNodeSecret( ); return response(res, { - data: null, + data: null, success: true, error: false, message: "Remote Exit Node secret updated successfully", diff --git a/server/private/routers/re-key/reGenerateSiteSecret.ts b/server/private/routers/re-key/reGenerateSiteSecret.ts index 4b58946e..62fb286d 100644 --- a/server/private/routers/re-key/reGenerateSiteSecret.ts +++ b/server/private/routers/re-key/reGenerateSiteSecret.ts @@ -36,25 +36,6 @@ const updateSiteBodySchema = z.strictObject({ pubKey: z.string().optional() }); -registry.registerPath({ - method: "post", - path: "/re-key/{siteId}/regenerate-site-secret", - description: - "Regenerate a site's Newt or WireGuard credentials by its site ID.", - tags: [OpenAPITags.Site], - request: { - params: updateSiteParamsSchema, - body: { - content: { - "application/json": { - schema: updateSiteBodySchema - } - } - } - }, - responses: {} -}); - export async function reGenerateSiteSecret( req: Request, res: Response, diff --git a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx index 0836d490..62c04116 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx @@ -1,11 +1,12 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; @@ -18,11 +19,26 @@ import { useTranslations } from "next-intl"; import { PickSiteDefaultsResponse } from "@server/routers/site"; import { useSiteContext } from "@app/hooks/useSiteContext"; import { generateKeypair } from "../wireguardConfig"; -import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal"; +import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { build } from "@server/build"; import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon } from "lucide-react"; +import { + generateWireGuardConfig, + generateObfuscatedWireGuardConfig +} from "@app/lib/wireguard"; +import { QRCodeCanvas } from "qrcode.react"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -37,6 +53,13 @@ export default function CredentialsPage() { useState(null); const [wgConfig, setWgConfig] = useState(""); const [publicKey, setPublicKey] = useState(""); + const [currentNewtId, setCurrentNewtId] = useState(null); + const [regeneratedSecret, setRegeneratedSecret] = useState( + null + ); + const [showCredentialsAlert, setShowCredentialsAlert] = useState(false); + const [showWireGuardAlert, setShowWireGuardAlert] = useState(false); + const [loadingDefaults, setLoadingDefaults] = useState(false); const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const subscription = useSubscriptionStatusContext(); @@ -48,145 +71,303 @@ export default function CredentialsPage() { return isEnterpriseNotLicensed || isSaasNotSubscribed; }; - const hydrateWireGuardConfig = ( - privateKey: string, - publicKey: string, - subnet: string, - address: string, - endpoint: string, - listenPort: string - ) => { - const config = `[Interface] -Address = ${subnet} -ListenPort = 51820 -PrivateKey = ${privateKey} + // Fetch site defaults for wireguard sites to show in obfuscated config + useEffect(() => { + const fetchSiteDefaults = async () => { + if (site?.type === "wireguard" && !siteDefaults && orgId) { + setLoadingDefaults(true); + try { + const res = await api.get(`/org/${orgId}/pick-site-defaults`); + if (res && res.status === 200) { + setSiteDefaults(res.data.data); + } + } catch (error) { + // Silently fail - we'll use site data or obfuscated values + } finally { + setLoadingDefaults(false); + } + } else { + setLoadingDefaults(false); + } + }; + fetchSiteDefaults(); + }, []); -[Peer] -PublicKey = ${publicKey} -AllowedIPs = ${address.split("/")[0]}/32 -Endpoint = ${endpoint}:${listenPort} -PersistentKeepalive = 5`; - setWgConfig(config); - return config; - }; const handleConfirmRegenerate = async () => { - let generatedPublicKey = ""; - let generatedWgConfig = ""; + try { + let generatedPublicKey = ""; + let generatedWgConfig = ""; - if (site?.type === "wireguard") { - const generatedKeypair = generateKeypair(); - generatedPublicKey = generatedKeypair.publicKey; - setPublicKey(generatedPublicKey); + if (site?.type === "wireguard") { + const generatedKeypair = generateKeypair(); + generatedPublicKey = generatedKeypair.publicKey; + setPublicKey(generatedPublicKey); - const res = await api.get(`/org/${orgId}/pick-site-defaults`); - if (res && res.status === 200) { - const data = res.data.data; - setSiteDefaults(data); + const res = await api.get(`/org/${orgId}/pick-site-defaults`); + if (res && res.status === 200) { + const data = res.data.data; + setSiteDefaults(data); - // generate config with the fetched data - generatedWgConfig = hydrateWireGuardConfig( - generatedKeypair.privateKey, - data.publicKey, - data.subnet, - data.address, - data.endpoint, - data.listenPort + // generate config with the fetched data + generatedWgConfig = generateWireGuardConfig( + generatedKeypair.privateKey, + data.publicKey, + data.subnet, + data.address, + data.endpoint, + data.listenPort + ); + setWgConfig(generatedWgConfig); + setShowWireGuardAlert(true); + } + + await api.post( + `/re-key/${site?.siteId}/regenerate-site-secret`, + { + type: "wireguard", + pubKey: generatedPublicKey + } ); } - await api.post(`/re-key/${site?.siteId}/regenerate-site-secret`, { - type: "wireguard", - pubKey: generatedPublicKey - }); - } + if (site?.type === "newt") { + const res = await api.get(`/org/${orgId}/pick-site-defaults`); + if (res && res.status === 200) { + const data = res.data.data; - if (site?.type === "newt") { - const res = await api.get(`/org/${orgId}/pick-site-defaults`); - if (res && res.status === 200) { - const data = res.data.data; + const rekeyRes = await api.post( + `/re-key/${site?.siteId}/regenerate-site-secret`, + { + type: "newt", + secret: data.newtSecret + } + ); - const rekeyRes = await api.post( - `/re-key/${site?.siteId}/regenerate-site-secret`, - { - type: "newt", - secret: data.newtSecret - } - ); - - if (rekeyRes && rekeyRes.status === 200) { - const rekeyData = rekeyRes.data.data; - if (rekeyData && rekeyData.newtId) { - setSiteDefaults({ - ...data, - newtId: rekeyData.newtId - }); + if (rekeyRes && rekeyRes.status === 200) { + const rekeyData = rekeyRes.data.data; + if (rekeyData && rekeyData.newtId) { + setCurrentNewtId(rekeyData.newtId); + setRegeneratedSecret(data.newtSecret); + setSiteDefaults({ + ...data, + newtId: rekeyData.newtId + }); + setShowCredentialsAlert(true); + } } } } + + toast({ + title: t("credentialsSaved"), + description: t("credentialsSavedDescription") + }); + + setModalOpen(false); + router.refresh(); + } catch (error) { + toast({ + variant: "destructive", + title: t("error") || "Error", + description: + formatAxiosError(error) || + t("credentialsRegenerateError") || + "Failed to regenerate credentials" + }); } - - toast({ - title: t("credentialsSaved"), - description: t("credentialsSavedDescription") - }); - - router.refresh(); }; - const getCredentialType = () => { - if (site?.type === "wireguard") return "site-wireguard"; - if (site?.type === "newt") return "site-newt"; - return "site-newt"; + const getConfirmationString = () => { + if (site?.type === "newt") { + return site?.niceId || site?.name || ""; + } + return site?.niceId || site?.name || ""; }; - const getCredentials = () => { - if (site?.type === "wireguard" && wgConfig) { - return { wgConfig }; - } - if (site?.type === "newt" && siteDefaults) { - return { - Id: siteDefaults.newtId, - Secret: siteDefaults.newtSecret - }; - } - return undefined; - }; + const displayNewtId = currentNewtId || siteDefaults?.newtId || null; + const displaySecret = regeneratedSecret || null; return ( <> - - - - {t("generatedcredentials")} - - - {t("regenerateCredentials")} - - + {site?.type === "newt" && ( + + + + {t("siteNewtCredentials")} + + + {t("siteNewtCredentialsDescription")} + + + + + + + {t("newtEndpoint")} + + + + + + + + {t("newtId")} + + + {displayNewtId ? ( + + ) : ( + {"••••••••••••••••"} + )} + + + + + {t("newtSecretKey")} + + + {displaySecret ? ( + + ) : ( + {"••••••••••••••••"} + )} + + + - + {showCredentialsAlert && displaySecret && ( + + + + {t("siteCredentialsSave")} + + + {t("siteCredentialsSaveDescription")} + + + )} + + + + + + )} - - - - + {site?.type === "wireguard" && ( + + + + {t("generatedcredentials")} + + + {t("regenerateCredentials")} + + + + + + + {!loadingDefaults && ( + <> + {wgConfig ? ( +
+ +
+
+ +
+
+
+ ) : ( + + )} + {showWireGuardAlert && wgConfig && ( + + + + {t("siteCredentialsSave")} + + + {t("siteCredentialsSaveDescription")} + + + )} + + )} +
+ + + +
+ )}
- + {site?.type === "newt" && ( + +

{t("regenerateCredentialsConfirmation")}

+

{t("regenerateCredentialsWarning")}

+ + } + buttonText={t("regenerateCredentialsButton")} + onConfirm={handleConfirmRegenerate} + string={getConfirmationString()} + title={t("regenerateCredentials")} + warningText={t("cannotbeUndone")} + /> + )} + + {site?.type === "wireguard" && ( + +

{t("regenerateCredentialsConfirmation")}

+

{t("regenerateCredentialsWarning")}

+ + } + buttonText={t("regenerateCredentialsButton")} + onConfirm={handleConfirmRegenerate} + string={getConfirmationString()} + title={t("regenerateCredentials")} + warningText={t("cannotbeUndone")} + /> + )} ); } diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index a1362e87..be3d5e68 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -47,6 +47,7 @@ import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { generateKeypair } from "../[niceId]/wireguardConfig"; import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { generateWireGuardConfig } from "@app/lib/wireguard"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { CreateSiteBody, @@ -214,26 +215,6 @@ export default function Page() { string | undefined >(); - const hydrateWireGuardConfig = ( - privateKey: string, - publicKey: string, - subnet: string, - address: string, - endpoint: string, - listenPort: string - ) => { - const wgConfig = `[Interface] -Address = ${subnet} -ListenPort = 51820 -PrivateKey = ${privateKey} - -[Peer] -PublicKey = ${publicKey} -AllowedIPs = ${address.split("/")[0]}/32 -Endpoint = ${endpoint}:${listenPort} -PersistentKeepalive = 5`; - setWgConfig(wgConfig); - }; const hydrateCommands = ( id: string, @@ -595,7 +576,7 @@ WantedBy=default.target` acceptClients ); - hydrateWireGuardConfig( + const wgConfig = generateWireGuardConfig( privateKey, data.publicKey, data.subnet, @@ -603,6 +584,7 @@ WantedBy=default.target` data.endpoint, data.listenPort ); + setWgConfig(wgConfig); setTunnelTypes((prev: any) => { return prev.map((item: any) => { diff --git a/src/lib/wireguard.ts b/src/lib/wireguard.ts new file mode 100644 index 00000000..8fe986ce --- /dev/null +++ b/src/lib/wireguard.ts @@ -0,0 +1,61 @@ +export function generateWireGuardConfig( + privateKey: string, + publicKey: string, + subnet: string, + address: string, + endpoint: string, + listenPort: string | number +): string { + const addressWithoutCidr = address.split("/")[0]; + const port = typeof listenPort === "number" ? listenPort : listenPort; + + return `[Interface] +Address = ${subnet} +ListenPort = 51820 +PrivateKey = ${privateKey} + +[Peer] +PublicKey = ${publicKey} +AllowedIPs = ${addressWithoutCidr}/32 +Endpoint = ${endpoint}:${port} +PersistentKeepalive = 5`; +} + +export function generateObfuscatedWireGuardConfig(options?: { + subnet?: string | null; + address?: string | null; + endpoint?: string | null; + listenPort?: number | string | null; + publicKey?: string | null; +}): string { + const obfuscate = (value: string | null | undefined, length: number = 20): string => { + return value || "•".repeat(length); + }; + + const obfuscateKey = (value: string | null | undefined): string => { + return value || "•".repeat(44); // Base64 key length + }; + + const subnet = options?.subnet || obfuscate(null, 20); + const subnetWithCidr = subnet.includes("•") + ? `${subnet}/32` + : (subnet.includes("/") ? subnet : `${subnet}/32`); + const address = options?.address ? options.address.split("/")[0] : obfuscate(null, 20); + const endpoint = obfuscate(options?.endpoint, 20); + const listenPort = options?.listenPort + ? (typeof options.listenPort === "number" ? options.listenPort : options.listenPort) + : 51820; + const publicKey = obfuscateKey(options?.publicKey); + + return `[Interface] +Address = ${subnetWithCidr} +ListenPort = 51820 +PrivateKey = ${obfuscateKey(null)} + +[Peer] +PublicKey = ${publicKey} +AllowedIPs = ${address}/32 +Endpoint = ${endpoint}:${listenPort} +PersistentKeepalive = 5`; +} +