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

47111
package-lock.json generated

File diff suppressed because it is too large Load Diff

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";
@@ -12,8 +12,8 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const getClientSchema = z.strictObject({
clientId: z.string().transform(stoi).pipe(z.int().positive())
});
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";
@@ -12,15 +12,15 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const getSiteSchema = z.strictObject({
siteId: z
.string()
.optional()
.transform(stoi)
.pipe(z.int().positive().optional())
.optional(),
niceId: z.string().optional(),
orgId: z.string().optional()
});
siteId: z
.string()
.optional()
.transform(stoi)
.pipe(z.int().positive().optional())
.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,39 +67,62 @@ export default function CredentialsPage() {
};
const handleConfirmRegenerate = async () => {
const response = await api.get<
AxiosResponse<PickRemoteExitNodeDefaultsResponse>
>(`/org/${orgId}/pick-remote-exit-node-defaults`);
try {
const response = await api.get<
AxiosResponse<PickRemoteExitNodeDefaultsResponse>
>(`/org/${orgId}/pick-remote-exit-node-defaults`);
const data = response.data.data;
setCredentials(data);
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
};
toast({
title: t("credentialsSaved"),
description: t("credentialsSavedDescription")
});
} 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>