From 2b8204fdc8b916546497b96f378d00b6173a69eb Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Fri, 7 Nov 2025 23:30:24 +0530 Subject: [PATCH] seperate credentials rekeying in modal for reuse --- messages/en-US.json | 8 +- .../[remoteExitNodeId]/credentials/page.tsx | 164 +++------ .../clients/[clientId]/credentials/page.tsx | 189 ++-------- .../sites/[niceId]/credentials/page.tsx | 337 +++++------------- src/components/RegenerateCredentialsModal.tsx | 216 +++++++++++ 5 files changed, 391 insertions(+), 523 deletions(-) create mode 100644 src/components/RegenerateCredentialsModal.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 7928b69b..dc4b429a 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2106,5 +2106,11 @@ "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." + "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.", + "confirm": "Confirm", + "regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?", + "endpoint": "Endpoint", + "id": "Id", + "SecretKey": "Secret Key" } 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 12a79dd6..b605ad35 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 @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { SettingsContainer, SettingsSection, @@ -10,9 +10,6 @@ import { SettingsSectionTitle } from "@app/components/Settings"; import { Button } from "@app/components/ui/button"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { InfoIcon } from "lucide-react"; -import CopyTextBox from "@app/components/CopyTextBox"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; @@ -24,6 +21,7 @@ import { QuickStartRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types"; import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext"; +import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -31,79 +29,44 @@ export default function CredentialsPage() { const { orgId } = useParams(); const router = useRouter(); const t = useTranslations(); - const { remoteExitNode, updateRemoteExitNode } = useRemoteExitNodeContext(); + const { remoteExitNode } = useRemoteExitNodeContext(); - const [credentials, setCredentials] = - useState(null); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [credentials, setCredentials] = useState(null); - // Clear credentials when user leaves/reloads - useEffect(() => { - const clearCreds = () => setCredentials(null); - window.addEventListener("beforeunload", clearCreds); - return () => window.removeEventListener("beforeunload", clearCreds); - }, []); + const handleConfirmRegenerate = async () => { + + const response = await api.get>( + `/org/${orgId}/pick-remote-exit-node-defaults` + ); - const handleRegenerate = async () => { - try { - setLoading(true); - const response = await api.get< - AxiosResponse - >(`/org/${orgId}/pick-remote-exit-node-defaults`); + const data = response.data.data; + setCredentials(data); - setCredentials(response.data.data); - toast({ - title: t("success"), - description: t("Credentials generated successfully."), - }); - } catch (error) { - toast({ - title: t("error"), - description: formatAxiosError( - error, - t("Failed to generate credentials") - ), - variant: "destructive", - }); - } finally { - setLoading(false); - } + await api.put>( + `/org/${orgId}/reGenerate-remote-exit-node-secret`, + { + remoteExitNodeId: remoteExitNode.remoteExitNodeId, + secret: data.secret, + } + ); + + toast({ + title: t("credentialsSaved"), + description: t("credentialsSavedDescription") + }); + + router.refresh(); }; - const handleSave = async () => { - if (!credentials) return; - - try { - setSaving(true); - - const response = await api.put< - AxiosResponse - >(`/org/${orgId}/reGenerate-remote-exit-node-secret`, { - remoteExitNodeId: remoteExitNode.remoteExitNodeId, - secret: credentials.secret, - }); - - toast({ - title: t("success"), - description: t("Credentials saved successfully."), - }); - - // For security, clear them from UI - setCredentials(null); - - } catch (error) { - toast({ - title: t("error"), - description: formatAxiosError( - error, - t("Failed to save credentials") - ), - variant: "destructive", - }); - } finally { - setSaving(false); + const getCredentials = () => { + if (credentials) { + return { + Id: remoteExitNode.remoteExitNodeId, + Secret: credentials.secret + }; } + return undefined; }; return ( @@ -114,58 +77,25 @@ export default function CredentialsPage() { {t("generatedcredentials")} - {t("regenerateClientCredentials")} + {t("regenerateCredentials")} - {!credentials ? ( - - ) : ( - <> - - - - - - {t("copyandsavethesecredentials")} - - - {t( - "copyandsavethesecredentialsdescription" - )} - - - -
- - -
- - )} +
+ + ); -} +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx index 7d34b5eb..e4a67544 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { SettingsContainer, SettingsSection, @@ -10,17 +10,14 @@ import { SettingsSectionTitle } from "@app/components/Settings"; import { Button } from "@app/components/ui/button"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { InfoIcon } from "lucide-react"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; -import CopyToClipboard from "@app/components/CopyToClipboard"; import { PickClientDefaultsResponse } from "@server/routers/client"; import { useClientContext } from "@app/hooks/useClientContext"; +import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -28,55 +25,21 @@ export default function CredentialsPage() { const { orgId } = useParams(); const router = useRouter(); const t = useTranslations(); - const [olmId, setOlmId] = useState(""); - const [olmSecret, setOlmSecret] = useState(""); - const { client, updateClient } = useClientContext(); + const { client } = useClientContext(); + + const [modalOpen, setModalOpen] = useState(false); + const [clientDefaults, setClientDefaults] = useState(null); - const [clientDefaults, setClientDefaults] = - useState(null); - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); + const handleConfirmRegenerate = async () => { + + const res = await api.get(`/org/${orgId}/pick-client-defaults`); + if (res && res.status === 200) { + const data = res.data.data; + setClientDefaults(data); - // Clear credentials when user leaves/reloads - useEffect(() => { - const clearCreds = () => { - setOlmId(""); - setOlmSecret(""); - }; - window.addEventListener("beforeunload", clearCreds); - return () => window.removeEventListener("beforeunload", clearCreds); - }, []); - - const handleRegenerate = async () => { - try { - setLoading(true); - await api - .get(`/org/${orgId}/pick-client-defaults`) - .then((res) => { - if (res && res.status === 200) { - const data = res.data.data; - - setClientDefaults(data); - - const olmId = data.olmId; - const olmSecret = data.olmSecret; - setOlmId(olmId); - setOlmSecret(olmSecret); - - } - }); - } finally { - setLoading(false); - } - }; - - const handleSave = async () => { - setLoading(true); - - try { await api.post(`/client/${client?.clientId}/regenerate-secret`, { - olmId: clientDefaults?.olmId, - secret: clientDefaults?.olmSecret, + olmId: data.olmId, + secret: data.olmSecret, }); toast({ @@ -85,20 +48,19 @@ export default function CredentialsPage() { }); router.refresh(); - } catch (e) { - toast({ - variant: "destructive", - title: t("credentialsSaveError"), - description: formatAxiosError( - e, - t("credentialsSaveErrorDescription") - ) - }); - } finally { - setLoading(false); } }; + const getCredentials = () => { + if (clientDefaults) { + return { + Id: clientDefaults.olmId, + Secret: clientDefaults.olmSecret + }; + } + return undefined; + }; + return ( @@ -112,97 +74,20 @@ export default function CredentialsPage() { - {!clientDefaults ? ( - - ) : ( - <> - - - - {t("clientOlmCredentials")} - - - {t("clientOlmCredentialsDescription")} - - - - - - - {t("olmEndpoint")} - - - - - - - - {t("olmId")} - - - - - - - - {t("olmSecretKey")} - - - - - - - - - - - {t("copyandsavethesecredentials")} - - - {t( - "copyandsavethesecredentialsdescription" - )} - - - - - -
- - -
- - )} +
+ +
); -} +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx index 1420680c..3942ef83 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { SettingsContainer, SettingsSection, @@ -10,20 +10,15 @@ import { SettingsSectionTitle } from "@app/components/Settings"; import { Button } from "@app/components/ui/button"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { InfoIcon } from "lucide-react"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { useParams, useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; -import CopyToClipboard from "@app/components/CopyToClipboard"; import { PickSiteDefaultsResponse } from "@server/routers/site"; import { useSiteContext } from "@app/hooks/useSiteContext"; -import CopyTextBox from "@app/components/CopyTextBox"; -import { QRCodeCanvas } from "qrcode.react"; import { generateKeypair } from "../wireguardConfig"; +import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal"; export default function CredentialsPage() { const { env } = useEnvContext(); @@ -31,17 +26,12 @@ export default function CredentialsPage() { const { orgId } = useParams(); const router = useRouter(); const t = useTranslations(); - const [newtId, setNewtId] = useState(""); - const [newtSecret, setNewtSecret] = useState(""); - const { site, updateSite } = useSiteContext(); + const { site } = useSiteContext(); + + const [modalOpen, setModalOpen] = useState(false); + const [siteDefaults, setSiteDefaults] = useState(null); const [wgConfig, setWgConfig] = useState(""); - const [siteDefaults, setSiteDefaults] = - useState(null); - - const [loading, setLoading] = useState(false); - const [saving, setSaving] = useState(false); const [publicKey, setPublicKey] = useState(""); - const [privateKey, setPrivateKey] = useState(""); const hydrateWireGuardConfig = ( privateKey: string, @@ -51,7 +41,7 @@ export default function CredentialsPage() { endpoint: string, listenPort: string ) => { - const wgConfig = `[Interface] + const config = `[Interface] Address = ${subnet} ListenPort = 51820 PrivateKey = ${privateKey} @@ -61,124 +51,83 @@ PublicKey = ${publicKey} AllowedIPs = ${address.split("/")[0]}/32 Endpoint = ${endpoint}:${listenPort} PersistentKeepalive = 5`; - setWgConfig(wgConfig); + setWgConfig(config); + return config; }; - - // Clear credentials when user leaves/reloads - useEffect(() => { - const clearCreds = () => { - setNewtId(""); - setNewtSecret(""); - }; - window.addEventListener("beforeunload", clearCreds); - return () => window.removeEventListener("beforeunload", clearCreds); - }, []); - - const handleRegenerate = async () => { - - const generatedKeypair = generateKeypair(); - - const privateKey = generatedKeypair.privateKey; - const publicKey = generatedKeypair.publicKey; - - setPrivateKey(privateKey); - setPublicKey(publicKey); - try { - setLoading(true); - await api - .get(`/org/${orgId}/pick-site-defaults`) - .then((res) => { - if (res && res.status === 200) { - const data = res.data.data; - - setSiteDefaults(data); - - const newtId = data.newtId; - const newtSecret = data.newtSecret; - setNewtId(newtId); - setNewtSecret(newtSecret); - - hydrateWireGuardConfig( - privateKey, - data.publicKey, - data.subnet, - data.address, - data.endpoint, - data.listenPort - ); - - } - }); - } finally { - setLoading(false); - } - }; - - const handleSave = async () => { - setLoading(true); - - let payload: any = {}; + const handleConfirmRegenerate = async () => { + let generatedPublicKey = ""; + let generatedWgConfig = ""; if (site?.type === "wireguard") { - if (!siteDefaults || !wgConfig) { - toast({ - variant: "destructive", - title: t("siteErrorCreate"), - description: t("siteErrorCreateKeyPair") - }); - setLoading(false); - return; + 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); + + // generate config with the fetched data + generatedWgConfig = hydrateWireGuardConfig( + generatedKeypair.privateKey, + data.publicKey, + data.subnet, + data.address, + data.endpoint, + data.listenPort + ); } - payload = { + await api.post(`/site/${site?.siteId}/regenerate-secret`, { type: "wireguard", - subnet: siteDefaults.subnet, - exitNodeId: siteDefaults.exitNodeId, - pubKey: publicKey - }; + subnet: res.data.data.subnet, + exitNodeId: res.data.data.exitNodeId, + pubKey: generatedPublicKey + }); } + if (site?.type === "newt") { - if (!siteDefaults) { - toast({ - variant: "destructive", - title: t("siteErrorCreate"), - description: t("siteErrorCreateDefaults") + const res = await api.get(`/org/${orgId}/pick-site-defaults`); + if (res && res.status === 200) { + const data = res.data.data; + setSiteDefaults(data); + + await api.post(`/site/${site?.siteId}/regenerate-secret`, { + type: "newt", + newtId: data.newtId, + newtSecret: data.newtSecret }); - setLoading(false); - return; } - - payload = { - type: "newt", - newtId: siteDefaults?.newtId, - newtSecret: siteDefaults?.newtSecret - }; } - try { - await api.post(`/site/${site?.siteId}/regenerate-secret`, payload); + toast({ + title: t("credentialsSaved"), + description: t("credentialsSavedDescription") + }); - toast({ - title: t("credentialsSaved"), - description: t("credentialsSavedDescription") - }); - - router.refresh(); - } catch (e) { - toast({ - variant: "destructive", - title: t("credentialsSaveError"), - description: formatAxiosError( - e, - t("credentialsSaveErrorDescription") - ) - }); - } finally { - setLoading(false); - } + router.refresh(); }; + const getCredentialType = () => { + if (site?.type === "wireguard") return "site-wireguard"; + if (site?.type === "newt") return "site-newt"; + return "site-newt"; + }; + + const getCredentials = () => { + if (site?.type === "wireguard" && wgConfig) { + return { wgConfig }; + } + if (site?.type === "newt" && siteDefaults) { + return { + Id: siteDefaults.newtId, + Secret: siteDefaults.newtSecret + }; + } + return undefined; + }; return ( @@ -193,141 +142,23 @@ PersistentKeepalive = 5`; - {!siteDefaults ? ( - - ) : ( - <> - {site.type === "wireguard" && ( - - - - {t("WgConfiguration")} - - - {t("WgConfigurationDescription")} - - - -
- -
-
- -
-
-
- - - - {t("copyandsavethesecredentials")} - - - {t( - "copyandsavethesecredentialsdescription" - )} - - -
-
- )} - {site.type === "newt" && ( - - - - {t("siteNewtCredentials")} - - - {t( - "siteNewtCredentialsDescription" - )} - - - - - - - {t("newtEndpoint")} - - - - - - - - {t("newtId")} - - - - - - - - {t("newtSecretKey")} - - - - - - - - - - - - {t("copyandsavethesecredentials")} - - - {t( - "copyandsavethesecredentialsdescription" - )} - - - - - )} - -
- - -
- - )} +
+ +
); -} +} \ No newline at end of file diff --git a/src/components/RegenerateCredentialsModal.tsx b/src/components/RegenerateCredentialsModal.tsx new file mode 100644 index 00000000..f485746b --- /dev/null +++ b/src/components/RegenerateCredentialsModal.tsx @@ -0,0 +1,216 @@ +"use client"; + +import { useState } from "react"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { Button } from "@app/components/ui/button"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { InfoIcon, AlertTriangle } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { InfoSection, InfoSectionContent, InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import CopyTextBox from "@app/components/CopyTextBox"; +import { QRCodeCanvas } from "qrcode.react"; + +type CredentialType = "site-wireguard" | "site-newt" | "client-olm" | "remote-exit-node"; + +interface RegenerateCredentialsModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + type: CredentialType; + onConfirmRegenerate: () => Promise; + dashboardUrl: string; + credentials?: { + // For WireGuard sites + wgConfig?: string; + + Id?: string; + Secret?: string; + }; +} + +export default function RegenerateCredentialsModal({ + open, + onOpenChange, + type, + onConfirmRegenerate, + dashboardUrl, + credentials +}: RegenerateCredentialsModalProps) { + const t = useTranslations(); + const [stage, setStage] = useState<"confirm" | "show">("confirm"); + const [loading, setLoading] = useState(false); + + const handleConfirm = async () => { + try { + setLoading(true); + await onConfirmRegenerate(); + setStage("show"); + } catch (error) { + } finally { + setLoading(false); + } + }; + + const handleClose = () => { + setStage("confirm"); + onOpenChange(false); + }; + + const getTitle = () => { + if (stage === "confirm") { + return t("regeneratecredentials"); + } + switch (type) { + case "site-wireguard": + return t("WgConfiguration"); + case "site-newt": + return t("siteNewtCredentials"); + case "client-olm": + return t("clientOlmCredentials"); + case "remote-exit-node": + return t("remoteExitNodeCreate.generate.title"); + } + }; + + const getDescription = () => { + if (stage === "confirm") { + return t("regenerateCredentialsWarning"); + } + switch (type) { + case "site-wireguard": + return t("WgConfigurationDescription"); + case "site-newt": + return t("siteNewtCredentialsDescription"); + case "client-olm": + return t("clientOlmCredentialsDescription"); + case "remote-exit-node": + return t("remoteExitNodeCreate.generate.description"); + } + }; + + return ( + + + + {getTitle()} + {getDescription()} + + + + {stage === "confirm" ? ( + + + + {t("warning")} + + + {t("regenerateCredentialsConfirmation")} + + + ) : ( + <> + {credentials?.wgConfig && ( +
+
+ +
+
+ +
+
+
+ + + + + {t("copyandsavethesecredentials")} + + + {t("copyandsavethesecredentialsdescription")} + + +
+ )} + + {credentials?.Id && credentials.Secret && ( +
+ + + + {t("endpoint")} + + + + + + + + {t("Id")} + + + + + + + + {t("SecretKey")} + + + + + + + + + + {t("copyandsavethesecredentials")} + + + {t("copyandsavethesecredentialsdescription")} + + +
+ + )} + + )} +
+ + + {stage === "confirm" ? ( + <> + + + + + + ) : ( + + )} + +
+
+ ); +} \ No newline at end of file