show id in credential regen

This commit is contained in:
miloschwartz
2025-12-06 12:07:43 -05:00
parent 1d303feca2
commit 00174be8c0
6 changed files with 23795 additions and 23572 deletions

105
package-lock.json generated
View File

@@ -23502,6 +23502,111 @@
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.7.tgz",
"integrity": "sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.7.tgz",
"integrity": "sha512-UP6CaDBcqaCBuiq/gfCEJw7sPEoX1aIjZHnBWN9v9qYHQdMKvCKcAVs4OX1vIjeE+tC5EIuwDTVIoXpUes29lg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.7.tgz",
"integrity": "sha512-NCslw3GrNIw7OgmRBxHtdWFQYhexoUCq+0oS2ccjyYLtcn1SzGzeM54jpTFonIMUjNbHmpKpziXnpxhSWLcmBA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.7.tgz",
"integrity": "sha512-nfymt+SE5cvtTrG9u1wdoxBr9bVB7mtKTcj0ltRn6gkP/2Nu1zM5ei8rwP9qKQP0Y//umK+TtkKgNtfboBxRrw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.7.tgz",
"integrity": "sha512-4IUO539b8FmF0odY6/SqANJdgwn1xs1GkPO5doZugwZ3ETF6JUdckk7RGmsfSf7ws8Qb2YB5It33mvNL/0acqA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.7.tgz",
"integrity": "sha512-CpJVTkYI3ZajQkC5vajM7/ApKJUOlm6uP4BknM3XKvJ7VXAvCqSjSLmM0LKdYzn6nBJVSjdclx8nYJSa3xlTgQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.5.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.7.tgz",
"integrity": "sha512-gMzgBX164I6DN+9/PGA+9dQiwmTkE4TloBNx8Kv9UiGARsr9Nba7IpcBRA1iTV9vwlYnrE3Uy6I7Aj6qLjQuqw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { clients, clientSitesAssociationsCache } from "@server/db";
import { db, olms } from "@server/db";
import { clients } from "@server/db";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -13,7 +13,7 @@ import { OpenAPITags, registry } from "@server/openApi";
const getClientSchema = z.strictObject({
clientId: z.string().transform(stoi).pipe(z.int().positive())
});
});
async function query(clientId: number) {
// Get the client
@@ -21,26 +21,20 @@ async function query(clientId: number) {
.select()
.from(clients)
.where(and(eq(clients.clientId, clientId)))
.leftJoin(olms, eq(clients.olmId, olms.olmId))
.limit(1);
if (!client) {
return null;
}
// Get the siteIds associated with this client
const sites = await db
.select({ siteId: clientSitesAssociationsCache.siteId })
.from(clientSitesAssociationsCache)
.where(eq(clientSitesAssociationsCache.clientId, clientId));
// Add the siteIds to the client object
return {
...client,
siteIds: sites.map((site) => site.siteId)
};
return client;
}
export type GetClientResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
export type GetClientResponse = NonNullable<
Awaited<ReturnType<typeof query>>
>["clients"] & {
olmId: string | null;
};
registry.registerPath({
method: "get",
@@ -82,8 +76,13 @@ export async function getClient(
);
}
const data: GetClientResponse = {
...client.clients,
olmId: client.olms ? client.olms.olmId : null
};
return response<GetClientResponse>(res, {
data: client,
data,
success: true,
error: false,
message: "Client retrieved successfully",

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { db, newts } from "@server/db";
import { sites } from "@server/db";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
@@ -20,7 +20,7 @@ const getSiteSchema = z.strictObject({
.optional(),
niceId: z.string().optional(),
orgId: z.string().optional()
});
});
async function query(siteId?: number, niceId?: string, orgId?: string) {
if (siteId) {
@@ -28,6 +28,7 @@ async function query(siteId?: number, niceId?: string, orgId?: string) {
.select()
.from(sites)
.where(eq(sites.siteId, siteId))
.leftJoin(newts, eq(sites.siteId, newts.siteId))
.limit(1);
return res;
} else if (niceId && orgId) {
@@ -35,12 +36,15 @@ async function query(siteId?: number, niceId?: string, orgId?: string) {
.select()
.from(sites)
.where(and(eq(sites.niceId, niceId), eq(sites.orgId, orgId)))
.leftJoin(newts, eq(sites.siteId, newts.siteId))
.limit(1);
return res;
}
}
export type GetSiteResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
export type GetSiteResponse = NonNullable<
Awaited<ReturnType<typeof query>>
>["sites"] & { newtId: string | null };
registry.registerPath({
method: "get",
@@ -94,8 +98,13 @@ export async function getSite(
return next(createHttpError(HttpCode.NOT_FOUND, "Site not found"));
}
const data: GetSiteResponse = {
...site.sites,
newtId: site.newt ? site.newt.newtId : null
};
return response<GetSiteResponse>(res, {
data: site,
data,
success: true,
error: false,
message: "Site retrieved successfully",

View File

@@ -6,6 +6,7 @@ import {
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
@@ -21,11 +22,20 @@ import {
QuickStartRemoteExitNodeResponse
} from "@server/routers/remoteExitNode/types";
import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext";
import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
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 { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon } from "lucide-react";
export default function CredentialsPage() {
const { env } = useEnvContext();
@@ -38,6 +48,13 @@ export default function CredentialsPage() {
const [modalOpen, setModalOpen] = useState(false);
const [credentials, setCredentials] =
useState<PickRemoteExitNodeDefaultsResponse | null>(null);
const [currentRemoteExitNodeId, setCurrentRemoteExitNodeId] = useState<
string | null
>(remoteExitNode.remoteExitNodeId);
const [regeneratedSecret, setRegeneratedSecret] = useState<string | null>(
null
);
const [showCredentialsAlert, setShowCredentialsAlert] = useState(false);
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
const subscription = useSubscriptionStatusContext();
@@ -50,6 +67,7 @@ export default function CredentialsPage() {
};
const handleConfirmRegenerate = async () => {
try {
const response = await api.get<
AxiosResponse<PickRemoteExitNodeDefaultsResponse>
>(`/org/${orgId}/pick-remote-exit-node-defaults`);
@@ -57,32 +75,54 @@ export default function CredentialsPage() {
const data = response.data.data;
setCredentials(data);
await api.put<AxiosResponse<QuickStartRemoteExitNodeResponse>>(
`/re-key/${orgId}/regenerate-remote-exit-node-secret`,
{
const rekeyRes = await api.put<
AxiosResponse<QuickStartRemoteExitNodeResponse>
>(`/re-key/${orgId}/regenerate-remote-exit-node-secret`, {
remoteExitNodeId: remoteExitNode.remoteExitNodeId,
secret: data.secret
});
if (rekeyRes && rekeyRes.status === 200) {
const rekeyData = rekeyRes.data.data;
if (rekeyData && rekeyData.remoteExitNodeId) {
setCurrentRemoteExitNodeId(rekeyData.remoteExitNodeId);
setRegeneratedSecret(data.secret);
setCredentials({
...data,
remoteExitNodeId: rekeyData.remoteExitNodeId
});
setShowCredentialsAlert(true);
}
}
);
toast({
title: t("credentialsSaved"),
description: t("credentialsSavedDescription")
});
router.refresh();
};
const getCredentials = () => {
if (credentials) {
return {
Id: remoteExitNode.remoteExitNodeId,
Secret: credentials.secret
};
} catch (error) {
toast({
variant: "destructive",
title: t("error") || "Error",
description:
formatAxiosError(error) ||
t("credentialsRegenerateError") ||
"Failed to regenerate credentials"
});
}
return undefined;
};
const getConfirmationString = () => {
return (
remoteExitNode?.name ||
remoteExitNode?.remoteExitNodeId ||
"My remote exit node"
);
};
const displayRemoteExitNodeId =
currentRemoteExitNodeId || remoteExitNode?.remoteExitNodeId || null;
const displaySecret = regeneratedSecret || null;
return (
<>
<SettingsContainer>
@@ -95,26 +135,96 @@ export default function CredentialsPage() {
{t("regenerateCredentials")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SecurityFeaturesAlert />
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
{t("endpoint") || "Endpoint"}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
text={env.app.dashboardUrl}
/>
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("remoteExitNodeId") ||
"Remote Exit Node ID"}
</InfoSectionTitle>
<InfoSectionContent>
{displayRemoteExitNodeId ? (
<CopyToClipboard
text={displayRemoteExitNodeId}
/>
) : (
<span>{"••••••••••••••••"}</span>
)}
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("secretKey") || "Secret Key"}
</InfoSectionTitle>
<InfoSectionContent>
{displaySecret ? (
<CopyToClipboard text={displaySecret} />
) : (
<span>
{"••••••••••••••••••••••••••••••••"}
</span>
)}
</InfoSectionContent>
</InfoSection>
</InfoSections>
{showCredentialsAlert && displaySecret && (
<Alert variant="neutral" className="mt-4">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
{t("credentialsSave") ||
"Save the Credentials"}
</AlertTitle>
<AlertDescription>
{t("credentialsSaveDescription") ||
"You will only be able to see this once. Make sure to copy it to a secure place."}
</AlertDescription>
</Alert>
)}
</SettingsSectionBody>
<SettingsSectionFooter>
<Button
onClick={() => setModalOpen(true)}
disabled={isSecurityFeatureDisabled()}
>
{t("regeneratecredentials")}
{t("regenerateCredentialsButton")}
</Button>
</SettingsSectionBody>
</SettingsSectionFooter>
</SettingsSection>
</SettingsContainer>
<RegenerateCredentialsModal
<ConfirmDeleteDialog
open={modalOpen}
onOpenChange={setModalOpen}
type="remote-exit-node"
onConfirmRegenerate={handleConfirmRegenerate}
dashboardUrl={env.app.dashboardUrl}
credentials={getCredentials()}
setOpen={(val) => {
setModalOpen(val);
// Prevent modal from reopening during refresh
if (!val) {
setTimeout(() => {
router.refresh();
}, 150);
}
}}
dialog={
<div className="space-y-2">
<p>{t("regenerateCredentialsConfirmation")}</p>
<p>{t("regenerateCredentialsWarning")}</p>
</div>
}
buttonText={t("regenerateCredentialsButton")}
onConfirm={handleConfirmRegenerate}
string={getConfirmationString()}
title={t("regenerateCredentials")}
warningText={t("cannotbeUndone")}
/>
</>
);

View File

@@ -44,7 +44,7 @@ export default function CredentialsPage() {
const [modalOpen, setModalOpen] = useState(false);
const [clientDefaults, setClientDefaults] =
useState<PickClientDefaultsResponse | null>(null);
const [currentOlmId, setCurrentOlmId] = useState<string | null>(null);
const [currentOlmId, setCurrentOlmId] = useState<string | null>(client.olmId);
const [regeneratedSecret, setRegeneratedSecret] = useState<string | null>(
null
);
@@ -154,7 +154,7 @@ export default function CredentialsPage() {
{displaySecret ? (
<CopyToClipboard text={displaySecret} />
) : (
<span>{"••••••••••••••••"}</span>
<span>{"••••••••••••••••••••••••••••••••"}</span>
)}
</InfoSectionContent>
</InfoSection>

View File

@@ -53,7 +53,7 @@ export default function CredentialsPage() {
useState<PickSiteDefaultsResponse | null>(null);
const [wgConfig, setWgConfig] = useState("");
const [publicKey, setPublicKey] = useState("");
const [currentNewtId, setCurrentNewtId] = useState<string | null>(null);
const [currentNewtId, setCurrentNewtId] = useState<string | null>(site.newtId);
const [regeneratedSecret, setRegeneratedSecret] = useState<string | null>(
null
);
@@ -233,7 +233,7 @@ export default function CredentialsPage() {
text={displaySecret}
/>
) : (
<span>{"••••••••••••••••"}</span>
<span>{"••••••••••••••••••••••••••••••••"}</span>
)}
</InfoSectionContent>
</InfoSection>