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 { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db, olms } from "@server/db";
import { clients, clientSitesAssociationsCache } from "@server/db"; import { clients } from "@server/db";
import { eq, and } from "drizzle-orm"; import { eq, and } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
@@ -12,8 +12,8 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
const getClientSchema = z.strictObject({ 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) { async function query(clientId: number) {
// Get the client // Get the client
@@ -21,26 +21,20 @@ async function query(clientId: number) {
.select() .select()
.from(clients) .from(clients)
.where(and(eq(clients.clientId, clientId))) .where(and(eq(clients.clientId, clientId)))
.leftJoin(olms, eq(clients.olmId, olms.olmId))
.limit(1); .limit(1);
if (!client) { if (!client) {
return null; return null;
} }
return client;
// 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)
};
} }
export type GetClientResponse = NonNullable<Awaited<ReturnType<typeof query>>>; export type GetClientResponse = NonNullable<
Awaited<ReturnType<typeof query>>
>["clients"] & {
olmId: string | null;
};
registry.registerPath({ registry.registerPath({
method: "get", 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, { return response<GetClientResponse>(res, {
data: client, data,
success: true, success: true,
error: false, error: false,
message: "Client retrieved successfully", message: "Client retrieved successfully",

View File

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

View File

@@ -6,6 +6,7 @@ import {
SettingsSection, SettingsSection,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionFooter,
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle SettingsSectionTitle
} from "@app/components/Settings"; } from "@app/components/Settings";
@@ -21,11 +22,20 @@ import {
QuickStartRemoteExitNodeResponse QuickStartRemoteExitNodeResponse
} from "@server/routers/remoteExitNode/types"; } from "@server/routers/remoteExitNode/types";
import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext"; 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 { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
import { build } from "@server/build"; import { build } from "@server/build";
import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; 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() { export default function CredentialsPage() {
const { env } = useEnvContext(); const { env } = useEnvContext();
@@ -38,6 +48,13 @@ export default function CredentialsPage() {
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [credentials, setCredentials] = const [credentials, setCredentials] =
useState<PickRemoteExitNodeDefaultsResponse | null>(null); 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 { licenseStatus, isUnlocked } = useLicenseStatusContext();
const subscription = useSubscriptionStatusContext(); const subscription = useSubscriptionStatusContext();
@@ -50,39 +67,62 @@ export default function CredentialsPage() {
}; };
const handleConfirmRegenerate = async () => { const handleConfirmRegenerate = async () => {
const response = await api.get< try {
AxiosResponse<PickRemoteExitNodeDefaultsResponse> const response = await api.get<
>(`/org/${orgId}/pick-remote-exit-node-defaults`); AxiosResponse<PickRemoteExitNodeDefaultsResponse>
>(`/org/${orgId}/pick-remote-exit-node-defaults`);
const data = response.data.data; const data = response.data.data;
setCredentials(data); setCredentials(data);
await api.put<AxiosResponse<QuickStartRemoteExitNodeResponse>>( const rekeyRes = await api.put<
`/re-key/${orgId}/regenerate-remote-exit-node-secret`, AxiosResponse<QuickStartRemoteExitNodeResponse>
{ >(`/re-key/${orgId}/regenerate-remote-exit-node-secret`, {
remoteExitNodeId: remoteExitNode.remoteExitNodeId, remoteExitNodeId: remoteExitNode.remoteExitNodeId,
secret: data.secret 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({ toast({
title: t("credentialsSaved"), title: t("credentialsSaved"),
description: t("credentialsSavedDescription") description: t("credentialsSavedDescription")
}); });
} catch (error) {
router.refresh(); toast({
}; variant: "destructive",
title: t("error") || "Error",
const getCredentials = () => { description:
if (credentials) { formatAxiosError(error) ||
return { t("credentialsRegenerateError") ||
Id: remoteExitNode.remoteExitNodeId, "Failed to regenerate credentials"
Secret: credentials.secret });
};
} }
return undefined;
}; };
const getConfirmationString = () => {
return (
remoteExitNode?.name ||
remoteExitNode?.remoteExitNodeId ||
"My remote exit node"
);
};
const displayRemoteExitNodeId =
currentRemoteExitNodeId || remoteExitNode?.remoteExitNodeId || null;
const displaySecret = regeneratedSecret || null;
return ( return (
<> <>
<SettingsContainer> <SettingsContainer>
@@ -95,26 +135,96 @@ export default function CredentialsPage() {
{t("regenerateCredentials")} {t("regenerateCredentials")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <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 <Button
onClick={() => setModalOpen(true)} onClick={() => setModalOpen(true)}
disabled={isSecurityFeatureDisabled()} disabled={isSecurityFeatureDisabled()}
> >
{t("regeneratecredentials")} {t("regenerateCredentialsButton")}
</Button> </Button>
</SettingsSectionBody> </SettingsSectionFooter>
</SettingsSection> </SettingsSection>
</SettingsContainer> </SettingsContainer>
<RegenerateCredentialsModal <ConfirmDeleteDialog
open={modalOpen} open={modalOpen}
onOpenChange={setModalOpen} setOpen={(val) => {
type="remote-exit-node" setModalOpen(val);
onConfirmRegenerate={handleConfirmRegenerate} // Prevent modal from reopening during refresh
dashboardUrl={env.app.dashboardUrl} if (!val) {
credentials={getCredentials()} 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 [modalOpen, setModalOpen] = useState(false);
const [clientDefaults, setClientDefaults] = const [clientDefaults, setClientDefaults] =
useState<PickClientDefaultsResponse | null>(null); 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>( const [regeneratedSecret, setRegeneratedSecret] = useState<string | null>(
null null
); );
@@ -154,7 +154,7 @@ export default function CredentialsPage() {
{displaySecret ? ( {displaySecret ? (
<CopyToClipboard text={displaySecret} /> <CopyToClipboard text={displaySecret} />
) : ( ) : (
<span>{"••••••••••••••••"}</span> <span>{"••••••••••••••••••••••••••••••••"}</span>
)} )}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>

View File

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