mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-09 12:16:36 +00:00
improve site regenerate cred ui
This commit is contained in:
@@ -2053,7 +2053,7 @@
|
|||||||
"pathRewriteStripLabel": "strip",
|
"pathRewriteStripLabel": "strip",
|
||||||
"sidebarEnableEnterpriseLicense": "Enable Enterprise License",
|
"sidebarEnableEnterpriseLicense": "Enable Enterprise License",
|
||||||
"cannotbeUndone": "This can not be undone.",
|
"cannotbeUndone": "This can not be undone.",
|
||||||
"toConfirm": "to confirm",
|
"toConfirm": "to confirm.",
|
||||||
"deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?",
|
"deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?",
|
||||||
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.",
|
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.",
|
||||||
"sidebarLogs": "Logs",
|
"sidebarLogs": "Logs",
|
||||||
@@ -2220,7 +2220,7 @@
|
|||||||
"regenerate": "Regenerate",
|
"regenerate": "Regenerate",
|
||||||
"credentials": "Credentials",
|
"credentials": "Credentials",
|
||||||
"savecredentials": "Save Credentials",
|
"savecredentials": "Save Credentials",
|
||||||
"regeneratecredentials": "Re-key",
|
"regenerateCredentialsButton": "Regenerate Credentials",
|
||||||
"regenerateCredentials": "Regenerate and save your credentials",
|
"regenerateCredentials": "Regenerate and save your credentials",
|
||||||
"generatedcredentials": "Generated Credentials",
|
"generatedcredentials": "Generated Credentials",
|
||||||
"copyandsavethesecredentials": "Copy and save these credentials",
|
"copyandsavethesecredentials": "Copy and save these credentials",
|
||||||
@@ -2229,7 +2229,7 @@
|
|||||||
"credentialsSavedDescription": "Credentials have been regenerated and saved successfully.",
|
"credentialsSavedDescription": "Credentials have been regenerated and saved successfully.",
|
||||||
"credentialsSaveError": "Credentials Save Error",
|
"credentialsSaveError": "Credentials Save Error",
|
||||||
"credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials.",
|
"credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials.",
|
||||||
"regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones. Make sure to update any configurations that use these credentials.",
|
"regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones and cause a disconnection. Make sure to update any configurations that use these credentials.",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?",
|
"regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
|
|||||||
@@ -36,24 +36,6 @@ const reGenerateSecretBodySchema = z.strictObject({
|
|||||||
|
|
||||||
export type ReGenerateSecretBody = z.infer<typeof reGenerateSecretBodySchema>;
|
export type ReGenerateSecretBody = z.infer<typeof reGenerateSecretBodySchema>;
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "post",
|
|
||||||
path: "/re-key/{clientId}/regenerate-client-secret",
|
|
||||||
description: "Regenerate a client's OLM credentials by its client ID.",
|
|
||||||
tags: [OpenAPITags.Client],
|
|
||||||
request: {
|
|
||||||
params: reGenerateSecretParamsSchema,
|
|
||||||
body: {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: reGenerateSecretBodySchema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function reGenerateClientSecret(
|
export async function reGenerateClientSecret(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -34,24 +34,6 @@ const bodySchema = z.strictObject({
|
|||||||
secret: z.string().length(48)
|
secret: z.string().length(48)
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "post",
|
|
||||||
path: "/re-key/{orgId}/regenerate-secret",
|
|
||||||
description: "Regenerate a exit node credentials by its org ID.",
|
|
||||||
tags: [OpenAPITags.Org],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema,
|
|
||||||
body: {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: bodySchema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function reGenerateExitNodeSecret(
|
export async function reGenerateExitNodeSecret(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
@@ -108,7 +90,7 @@ export async function reGenerateExitNodeSecret(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Remote Exit Node secret updated successfully",
|
message: "Remote Exit Node secret updated successfully",
|
||||||
|
|||||||
@@ -36,25 +36,6 @@ const updateSiteBodySchema = z.strictObject({
|
|||||||
pubKey: z.string().optional()
|
pubKey: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "post",
|
|
||||||
path: "/re-key/{siteId}/regenerate-site-secret",
|
|
||||||
description:
|
|
||||||
"Regenerate a site's Newt or WireGuard credentials by its site ID.",
|
|
||||||
tags: [OpenAPITags.Site],
|
|
||||||
request: {
|
|
||||||
params: updateSiteParamsSchema,
|
|
||||||
body: {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: updateSiteBodySchema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function reGenerateSiteSecret(
|
export async function reGenerateSiteSecret(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
SettingsSectionBody,
|
SettingsSectionBody,
|
||||||
SettingsSectionDescription,
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionFooter,
|
||||||
SettingsSectionHeader,
|
SettingsSectionHeader,
|
||||||
SettingsSectionTitle
|
SettingsSectionTitle
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
@@ -18,11 +19,26 @@ import { useTranslations } from "next-intl";
|
|||||||
import { PickSiteDefaultsResponse } from "@server/routers/site";
|
import { PickSiteDefaultsResponse } from "@server/routers/site";
|
||||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||||
import { generateKeypair } from "../wireguardConfig";
|
import { generateKeypair } from "../wireguardConfig";
|
||||||
import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||||
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 CopyTextBox from "@app/components/CopyTextBox";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
generateWireGuardConfig,
|
||||||
|
generateObfuscatedWireGuardConfig
|
||||||
|
} from "@app/lib/wireguard";
|
||||||
|
import { QRCodeCanvas } from "qrcode.react";
|
||||||
|
|
||||||
export default function CredentialsPage() {
|
export default function CredentialsPage() {
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
@@ -37,6 +53,13 @@ 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 [regeneratedSecret, setRegeneratedSecret] = useState<string | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [showCredentialsAlert, setShowCredentialsAlert] = useState(false);
|
||||||
|
const [showWireGuardAlert, setShowWireGuardAlert] = useState(false);
|
||||||
|
const [loadingDefaults, setLoadingDefaults] = useState(false);
|
||||||
|
|
||||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||||
const subscription = useSubscriptionStatusContext();
|
const subscription = useSubscriptionStatusContext();
|
||||||
@@ -48,145 +71,303 @@ export default function CredentialsPage() {
|
|||||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const hydrateWireGuardConfig = (
|
// Fetch site defaults for wireguard sites to show in obfuscated config
|
||||||
privateKey: string,
|
useEffect(() => {
|
||||||
publicKey: string,
|
const fetchSiteDefaults = async () => {
|
||||||
subnet: string,
|
if (site?.type === "wireguard" && !siteDefaults && orgId) {
|
||||||
address: string,
|
setLoadingDefaults(true);
|
||||||
endpoint: string,
|
try {
|
||||||
listenPort: string
|
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
||||||
) => {
|
if (res && res.status === 200) {
|
||||||
const config = `[Interface]
|
setSiteDefaults(res.data.data);
|
||||||
Address = ${subnet}
|
}
|
||||||
ListenPort = 51820
|
} catch (error) {
|
||||||
PrivateKey = ${privateKey}
|
// Silently fail - we'll use site data or obfuscated values
|
||||||
|
} finally {
|
||||||
|
setLoadingDefaults(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setLoadingDefaults(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchSiteDefaults();
|
||||||
|
}, []);
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = ${publicKey}
|
|
||||||
AllowedIPs = ${address.split("/")[0]}/32
|
|
||||||
Endpoint = ${endpoint}:${listenPort}
|
|
||||||
PersistentKeepalive = 5`;
|
|
||||||
setWgConfig(config);
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleConfirmRegenerate = async () => {
|
const handleConfirmRegenerate = async () => {
|
||||||
let generatedPublicKey = "";
|
try {
|
||||||
let generatedWgConfig = "";
|
let generatedPublicKey = "";
|
||||||
|
let generatedWgConfig = "";
|
||||||
|
|
||||||
if (site?.type === "wireguard") {
|
if (site?.type === "wireguard") {
|
||||||
const generatedKeypair = generateKeypair();
|
const generatedKeypair = generateKeypair();
|
||||||
generatedPublicKey = generatedKeypair.publicKey;
|
generatedPublicKey = generatedKeypair.publicKey;
|
||||||
setPublicKey(generatedPublicKey);
|
setPublicKey(generatedPublicKey);
|
||||||
|
|
||||||
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
||||||
if (res && res.status === 200) {
|
if (res && res.status === 200) {
|
||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
setSiteDefaults(data);
|
setSiteDefaults(data);
|
||||||
|
|
||||||
// generate config with the fetched data
|
// generate config with the fetched data
|
||||||
generatedWgConfig = hydrateWireGuardConfig(
|
generatedWgConfig = generateWireGuardConfig(
|
||||||
generatedKeypair.privateKey,
|
generatedKeypair.privateKey,
|
||||||
data.publicKey,
|
data.publicKey,
|
||||||
data.subnet,
|
data.subnet,
|
||||||
data.address,
|
data.address,
|
||||||
data.endpoint,
|
data.endpoint,
|
||||||
data.listenPort
|
data.listenPort
|
||||||
|
);
|
||||||
|
setWgConfig(generatedWgConfig);
|
||||||
|
setShowWireGuardAlert(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.post(
|
||||||
|
`/re-key/${site?.siteId}/regenerate-site-secret`,
|
||||||
|
{
|
||||||
|
type: "wireguard",
|
||||||
|
pubKey: generatedPublicKey
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.post(`/re-key/${site?.siteId}/regenerate-site-secret`, {
|
if (site?.type === "newt") {
|
||||||
type: "wireguard",
|
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
||||||
pubKey: generatedPublicKey
|
if (res && res.status === 200) {
|
||||||
});
|
const data = res.data.data;
|
||||||
}
|
|
||||||
|
|
||||||
if (site?.type === "newt") {
|
const rekeyRes = await api.post(
|
||||||
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
`/re-key/${site?.siteId}/regenerate-site-secret`,
|
||||||
if (res && res.status === 200) {
|
{
|
||||||
const data = res.data.data;
|
type: "newt",
|
||||||
|
secret: data.newtSecret
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const rekeyRes = await api.post(
|
if (rekeyRes && rekeyRes.status === 200) {
|
||||||
`/re-key/${site?.siteId}/regenerate-site-secret`,
|
const rekeyData = rekeyRes.data.data;
|
||||||
{
|
if (rekeyData && rekeyData.newtId) {
|
||||||
type: "newt",
|
setCurrentNewtId(rekeyData.newtId);
|
||||||
secret: data.newtSecret
|
setRegeneratedSecret(data.newtSecret);
|
||||||
}
|
setSiteDefaults({
|
||||||
);
|
...data,
|
||||||
|
newtId: rekeyData.newtId
|
||||||
if (rekeyRes && rekeyRes.status === 200) {
|
});
|
||||||
const rekeyData = rekeyRes.data.data;
|
setShowCredentialsAlert(true);
|
||||||
if (rekeyData && rekeyData.newtId) {
|
}
|
||||||
setSiteDefaults({
|
|
||||||
...data,
|
|
||||||
newtId: rekeyData.newtId
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: t("credentialsSaved"),
|
||||||
|
description: t("credentialsSavedDescription")
|
||||||
|
});
|
||||||
|
|
||||||
|
setModalOpen(false);
|
||||||
|
router.refresh();
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: t("error") || "Error",
|
||||||
|
description:
|
||||||
|
formatAxiosError(error) ||
|
||||||
|
t("credentialsRegenerateError") ||
|
||||||
|
"Failed to regenerate credentials"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toast({
|
|
||||||
title: t("credentialsSaved"),
|
|
||||||
description: t("credentialsSavedDescription")
|
|
||||||
});
|
|
||||||
|
|
||||||
router.refresh();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCredentialType = () => {
|
const getConfirmationString = () => {
|
||||||
if (site?.type === "wireguard") return "site-wireguard";
|
if (site?.type === "newt") {
|
||||||
if (site?.type === "newt") return "site-newt";
|
return site?.niceId || site?.name || "";
|
||||||
return "site-newt";
|
}
|
||||||
|
return site?.niceId || site?.name || "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCredentials = () => {
|
const displayNewtId = currentNewtId || siteDefaults?.newtId || null;
|
||||||
if (site?.type === "wireguard" && wgConfig) {
|
const displaySecret = regeneratedSecret || null;
|
||||||
return { wgConfig };
|
|
||||||
}
|
|
||||||
if (site?.type === "newt" && siteDefaults) {
|
|
||||||
return {
|
|
||||||
Id: siteDefaults.newtId,
|
|
||||||
Secret: siteDefaults.newtSecret
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
{site?.type === "newt" && (
|
||||||
<SettingsSectionHeader>
|
<SettingsSection>
|
||||||
<SettingsSectionTitle>
|
<SettingsSectionHeader>
|
||||||
{t("generatedcredentials")}
|
<SettingsSectionTitle>
|
||||||
</SettingsSectionTitle>
|
{t("siteNewtCredentials")}
|
||||||
<SettingsSectionDescription>
|
</SettingsSectionTitle>
|
||||||
{t("regenerateCredentials")}
|
<SettingsSectionDescription>
|
||||||
</SettingsSectionDescription>
|
{t("siteNewtCredentialsDescription")}
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<InfoSections cols={3}>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>
|
||||||
|
{t("newtEndpoint")}
|
||||||
|
</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
<CopyToClipboard
|
||||||
|
text={env.app.dashboardUrl}
|
||||||
|
/>
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>
|
||||||
|
{t("newtId")}
|
||||||
|
</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
{displayNewtId ? (
|
||||||
|
<CopyToClipboard
|
||||||
|
text={displayNewtId}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>{"••••••••••••••••"}</span>
|
||||||
|
)}
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle>
|
||||||
|
{t("newtSecretKey")}
|
||||||
|
</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
{displaySecret ? (
|
||||||
|
<CopyToClipboard
|
||||||
|
text={displaySecret}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>{"••••••••••••••••"}</span>
|
||||||
|
)}
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
</InfoSections>
|
||||||
|
|
||||||
<SecurityFeaturesAlert />
|
{showCredentialsAlert && displaySecret && (
|
||||||
|
<Alert variant="neutral" className="mt-4">
|
||||||
|
<InfoIcon className="h-4 w-4" />
|
||||||
|
<AlertTitle className="font-semibold">
|
||||||
|
{t("siteCredentialsSave")}
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{t("siteCredentialsSaveDescription")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</SettingsSectionBody>
|
||||||
|
<SettingsSectionFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => setModalOpen(true)}
|
||||||
|
disabled={isSecurityFeatureDisabled()}
|
||||||
|
>
|
||||||
|
{t("regenerateCredentialsButton")}
|
||||||
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
<SettingsSectionBody>
|
{site?.type === "wireguard" && (
|
||||||
<Button
|
<SettingsSection>
|
||||||
onClick={() => setModalOpen(true)}
|
<SettingsSectionHeader>
|
||||||
disabled={isSecurityFeatureDisabled()}
|
<SettingsSectionTitle>
|
||||||
>
|
{t("generatedcredentials")}
|
||||||
{t("regeneratecredentials")}
|
</SettingsSectionTitle>
|
||||||
</Button>
|
<SettingsSectionDescription>
|
||||||
</SettingsSectionBody>
|
{t("regenerateCredentials")}
|
||||||
</SettingsSection>
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
|
||||||
|
<SecurityFeaturesAlert />
|
||||||
|
|
||||||
|
<SettingsSectionBody>
|
||||||
|
{!loadingDefaults && (
|
||||||
|
<>
|
||||||
|
{wgConfig ? (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<CopyTextBox text={wgConfig} outline={true} />
|
||||||
|
<div className="relative w-fit border rounded-md">
|
||||||
|
<div className="bg-white p-6 rounded-md">
|
||||||
|
<QRCodeCanvas
|
||||||
|
value={wgConfig}
|
||||||
|
size={168}
|
||||||
|
className="mx-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CopyTextBox
|
||||||
|
text={generateObfuscatedWireGuardConfig({
|
||||||
|
subnet: siteDefaults?.subnet || site?.subnet || null,
|
||||||
|
address: siteDefaults?.address || site?.address || null,
|
||||||
|
endpoint: siteDefaults?.endpoint || site?.endpoint || null,
|
||||||
|
listenPort: siteDefaults?.listenPort || site?.listenPort || null,
|
||||||
|
publicKey: siteDefaults?.publicKey || site?.publicKey || site?.pubKey || null
|
||||||
|
})}
|
||||||
|
outline={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showWireGuardAlert && wgConfig && (
|
||||||
|
<Alert variant="neutral" className="mt-4">
|
||||||
|
<InfoIcon className="h-4 w-4" />
|
||||||
|
<AlertTitle className="font-semibold">
|
||||||
|
{t("siteCredentialsSave")}
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
{t("siteCredentialsSaveDescription")}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SettingsSectionBody>
|
||||||
|
<SettingsSectionFooter>
|
||||||
|
<Button
|
||||||
|
onClick={() => setModalOpen(true)}
|
||||||
|
disabled={isSecurityFeatureDisabled()}
|
||||||
|
>
|
||||||
|
{t("regenerateCredentialsButton")}
|
||||||
|
</Button>
|
||||||
|
</SettingsSectionFooter>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|
||||||
<RegenerateCredentialsModal
|
{site?.type === "newt" && (
|
||||||
open={modalOpen}
|
<ConfirmDeleteDialog
|
||||||
onOpenChange={setModalOpen}
|
open={modalOpen}
|
||||||
type={getCredentialType()}
|
setOpen={setModalOpen}
|
||||||
onConfirmRegenerate={handleConfirmRegenerate}
|
dialog={
|
||||||
dashboardUrl={env.app.dashboardUrl}
|
<div className="space-y-2">
|
||||||
credentials={getCredentials()}
|
<p>{t("regenerateCredentialsConfirmation")}</p>
|
||||||
/>
|
<p>{t("regenerateCredentialsWarning")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("regenerateCredentialsButton")}
|
||||||
|
onConfirm={handleConfirmRegenerate}
|
||||||
|
string={getConfirmationString()}
|
||||||
|
title={t("regenerateCredentials")}
|
||||||
|
warningText={t("cannotbeUndone")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{site?.type === "wireguard" && (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={modalOpen}
|
||||||
|
setOpen={setModalOpen}
|
||||||
|
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")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox";
|
|||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
import { generateKeypair } from "../[niceId]/wireguardConfig";
|
import { generateKeypair } from "../[niceId]/wireguardConfig";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { generateWireGuardConfig } from "@app/lib/wireguard";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import {
|
import {
|
||||||
CreateSiteBody,
|
CreateSiteBody,
|
||||||
@@ -214,26 +215,6 @@ export default function Page() {
|
|||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const hydrateWireGuardConfig = (
|
|
||||||
privateKey: string,
|
|
||||||
publicKey: string,
|
|
||||||
subnet: string,
|
|
||||||
address: string,
|
|
||||||
endpoint: string,
|
|
||||||
listenPort: string
|
|
||||||
) => {
|
|
||||||
const wgConfig = `[Interface]
|
|
||||||
Address = ${subnet}
|
|
||||||
ListenPort = 51820
|
|
||||||
PrivateKey = ${privateKey}
|
|
||||||
|
|
||||||
[Peer]
|
|
||||||
PublicKey = ${publicKey}
|
|
||||||
AllowedIPs = ${address.split("/")[0]}/32
|
|
||||||
Endpoint = ${endpoint}:${listenPort}
|
|
||||||
PersistentKeepalive = 5`;
|
|
||||||
setWgConfig(wgConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hydrateCommands = (
|
const hydrateCommands = (
|
||||||
id: string,
|
id: string,
|
||||||
@@ -595,7 +576,7 @@ WantedBy=default.target`
|
|||||||
acceptClients
|
acceptClients
|
||||||
);
|
);
|
||||||
|
|
||||||
hydrateWireGuardConfig(
|
const wgConfig = generateWireGuardConfig(
|
||||||
privateKey,
|
privateKey,
|
||||||
data.publicKey,
|
data.publicKey,
|
||||||
data.subnet,
|
data.subnet,
|
||||||
@@ -603,6 +584,7 @@ WantedBy=default.target`
|
|||||||
data.endpoint,
|
data.endpoint,
|
||||||
data.listenPort
|
data.listenPort
|
||||||
);
|
);
|
||||||
|
setWgConfig(wgConfig);
|
||||||
|
|
||||||
setTunnelTypes((prev: any) => {
|
setTunnelTypes((prev: any) => {
|
||||||
return prev.map((item: any) => {
|
return prev.map((item: any) => {
|
||||||
|
|||||||
61
src/lib/wireguard.ts
Normal file
61
src/lib/wireguard.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
export function generateWireGuardConfig(
|
||||||
|
privateKey: string,
|
||||||
|
publicKey: string,
|
||||||
|
subnet: string,
|
||||||
|
address: string,
|
||||||
|
endpoint: string,
|
||||||
|
listenPort: string | number
|
||||||
|
): string {
|
||||||
|
const addressWithoutCidr = address.split("/")[0];
|
||||||
|
const port = typeof listenPort === "number" ? listenPort : listenPort;
|
||||||
|
|
||||||
|
return `[Interface]
|
||||||
|
Address = ${subnet}
|
||||||
|
ListenPort = 51820
|
||||||
|
PrivateKey = ${privateKey}
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = ${publicKey}
|
||||||
|
AllowedIPs = ${addressWithoutCidr}/32
|
||||||
|
Endpoint = ${endpoint}:${port}
|
||||||
|
PersistentKeepalive = 5`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateObfuscatedWireGuardConfig(options?: {
|
||||||
|
subnet?: string | null;
|
||||||
|
address?: string | null;
|
||||||
|
endpoint?: string | null;
|
||||||
|
listenPort?: number | string | null;
|
||||||
|
publicKey?: string | null;
|
||||||
|
}): string {
|
||||||
|
const obfuscate = (value: string | null | undefined, length: number = 20): string => {
|
||||||
|
return value || "•".repeat(length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const obfuscateKey = (value: string | null | undefined): string => {
|
||||||
|
return value || "•".repeat(44); // Base64 key length
|
||||||
|
};
|
||||||
|
|
||||||
|
const subnet = options?.subnet || obfuscate(null, 20);
|
||||||
|
const subnetWithCidr = subnet.includes("•")
|
||||||
|
? `${subnet}/32`
|
||||||
|
: (subnet.includes("/") ? subnet : `${subnet}/32`);
|
||||||
|
const address = options?.address ? options.address.split("/")[0] : obfuscate(null, 20);
|
||||||
|
const endpoint = obfuscate(options?.endpoint, 20);
|
||||||
|
const listenPort = options?.listenPort
|
||||||
|
? (typeof options.listenPort === "number" ? options.listenPort : options.listenPort)
|
||||||
|
: 51820;
|
||||||
|
const publicKey = obfuscateKey(options?.publicKey);
|
||||||
|
|
||||||
|
return `[Interface]
|
||||||
|
Address = ${subnetWithCidr}
|
||||||
|
ListenPort = 51820
|
||||||
|
PrivateKey = ${obfuscateKey(null)}
|
||||||
|
|
||||||
|
[Peer]
|
||||||
|
PublicKey = ${publicKey}
|
||||||
|
AllowedIPs = ${address}/32
|
||||||
|
Endpoint = ${endpoint}:${listenPort}
|
||||||
|
PersistentKeepalive = 5`;
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user