diff --git a/messages/en-US.json b/messages/en-US.json index 063d9efc..9b2c35f8 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2095,5 +2095,11 @@ "selectedResources": "Selected Resources", "enableSelected": "Enable Selected", "disableSelected": "Disable Selected", - "checkSelectedStatus": "Check Status of Selected" + "checkSelectedStatus": "Check Status of Selected", + "savecredentials": "Save Credentials", + "regeneratecredentials": "Regenerate Credentials", + "regenerateClientCredentials": "Regenerate and save your managed 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." } diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts index 884a9864..84e5f619 100644 --- a/server/routers/client/updateClient.ts +++ b/server/routers/client/updateClient.ts @@ -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"; @@ -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({ @@ -30,7 +31,10 @@ const updateClientSchema = z name: z.string().min(1).max(255).optional(), siteIds: z .array(z.number().int().positive()) - .optional() + .optional(), + olmId: z.string().min(1).optional(), + secret: z.string().min(1).optional(), + }) .strict(); @@ -75,7 +79,7 @@ export async function updateClient( ); } - const { name, siteIds } = parsedBody.data; + const { name, siteIds, olmId, secret } = parsedBody.data; const parsedParams = updateClientParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -89,6 +93,12 @@ export async function updateClient( 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() @@ -136,6 +146,22 @@ export async function updateClient( .where(eq(clients.clientId, clientId)); } + const [existingOlm] = await trx + .select() + .from(olms) + .where(eq(olms.clientId, clientId)) + .limit(1); + + if (existingOlm && olmId && secretHash) { + await trx + .update(olms) + .set({ + olmId, + secretHash + }) + .where(eq(olms.clientId, clientId)); + } + // Update site associations if provided // Remove sites that are no longer associated for (const siteId of sitesRemoved) { diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx index 6711177b..d207f0de 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx @@ -111,10 +111,10 @@ export default function GeneralPage() { - {t("Generated Credentials")} + {t("generatedcredentials")} - {t("Regenerate and save your managed credentials")} + {t("regenerateClientCredentials")} @@ -125,7 +125,7 @@ export default function GeneralPage() { loading={loading} disabled={loading} > - {t("Regenerate Credentials")} + {t("regeneratecredentials")} ) : ( <> @@ -138,11 +138,11 @@ export default function GeneralPage() { - {t("Copy and save these credentials")} + {t("copyandsavethesecredentials")} {t( - "These credentials will not be shown again after you leave this page. Save them securely now." + "copyandsavethesecredentialsdescription" )} @@ -152,14 +152,14 @@ export default function GeneralPage() { variant="outline" onClick={() => setCredentials(null)} > - {t("Cancel")} + {t("cancel")} diff --git a/src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx b/src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx new file mode 100644 index 00000000..fc86a93c --- /dev/null +++ b/src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx @@ -0,0 +1,208 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionHeader, + 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"; + +export default function GeneralPage() { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const { orgId } = useParams(); + const router = useRouter(); + const t = useTranslations(); + const [olmId, setOlmId] = useState(""); + const [olmSecret, setOlmSecret] = useState(""); + const { client, updateClient } = useClientContext(); + + const [clientDefaults, setClientDefaults] = + useState(null); + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + + // 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}`, { + olmId: clientDefaults?.olmId, + secret: clientDefaults?.olmSecret, + }); + + toast({ + title: t("clientUpdated"), + description: t("clientUpdatedDescription") + }); + + router.refresh(); + } catch (e) { + toast({ + variant: "destructive", + title: t("clientUpdateFailed"), + description: formatAxiosError( + e, + t("clientUpdateError") + ) + }); + } finally { + setLoading(false); + } + }; + + return ( + + + + + {t("generatedcredentials")} + + + {t("regenerateClientCredentials")} + + + + + {!clientDefaults ? ( + + ) : ( + <> + + + + {t("clientOlmCredentials")} + + + {t("clientOlmCredentialsDescription")} + + + + + + + {t("olmEndpoint")} + + + + + + + + {t("olmId")} + + + + + + + + {t("olmSecretKey")} + + + + + + + + + + + {t("copyandsavethesecredentials")} + + + {t( + "copyandsavethesecredentialsdescription" + )} + + + + + +
+ + +
+ + )} +
+
+
+ ); +} diff --git a/src/app/[orgId]/settings/clients/[clientId]/layout.tsx b/src/app/[orgId]/settings/clients/[clientId]/layout.tsx index c9c9fd14..e597f90d 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/layout.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/layout.tsx @@ -34,6 +34,10 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { { title: "General", href: `/{orgId}/settings/clients/{clientId}/general` + }, + { + title: "Credentials", + href: `/{orgId}/settings/clients/{clientId}/credentials` } ];