mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-09 20:26:40 +00:00
show id in credential regen
This commit is contained in:
47111
package-lock.json
generated
47111
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user