mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-01 16:26:39 +00:00
Merge branch 'dev' into feat/login-page-customization
This commit is contained in:
@@ -64,10 +64,8 @@ export default function Page() {
|
||||
clientSecret: z
|
||||
.string()
|
||||
.min(1, { message: t("idpClientSecretRequired") }),
|
||||
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") })
|
||||
.optional(),
|
||||
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") })
|
||||
.optional(),
|
||||
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }).optional(),
|
||||
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }).optional(),
|
||||
identifierPath: z
|
||||
.string()
|
||||
.min(1, { message: t("idpPathRequired") })
|
||||
@@ -379,9 +377,11 @@ export default function Page() {
|
||||
>
|
||||
<AutoProvisionConfigWidget
|
||||
control={form.control}
|
||||
autoProvision={form.watch(
|
||||
"autoProvision"
|
||||
) as boolean} // is this right?
|
||||
autoProvision={
|
||||
form.watch(
|
||||
"autoProvision"
|
||||
) as boolean
|
||||
} // is this right?
|
||||
onAutoProvisionChange={(checked) => {
|
||||
form.setValue(
|
||||
"autoProvision",
|
||||
|
||||
@@ -19,18 +19,17 @@ export function ExitNodesDataTable<TData, TValue>({
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
title={t('remoteExitNodes')}
|
||||
searchPlaceholder={t('searchRemoteExitNodes')}
|
||||
title={t("remoteExitNodes")}
|
||||
searchPlaceholder={t("searchRemoteExitNodes")}
|
||||
searchColumn="name"
|
||||
onAdd={createRemoteExitNode}
|
||||
addButtonText={t('remoteExitNodeAdd')}
|
||||
addButtonText={t("remoteExitNodeAdd")}
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
defaultSort={{
|
||||
|
||||
@@ -231,12 +231,22 @@ export default function ExitNodesTable({
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
return originalRow.version || "-";
|
||||
return (
|
||||
<div className="flex items-center space-x-1">
|
||||
{originalRow.version && originalRow.version ? (
|
||||
<Badge variant="secondary">
|
||||
{"v" + originalRow.version}
|
||||
</Badge>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
header: () => <span className="p-3">{t("actions")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const nodeRow = row.original;
|
||||
const remoteExitNodeId = nodeRow.id;
|
||||
@@ -294,10 +304,8 @@ export default function ExitNodesTable({
|
||||
setSelectedNode(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("remoteExitNodeQuestionRemove")}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<p>{t("remoteExitNodeQuestionRemove")}</p>
|
||||
|
||||
<p>{t("remoteExitNodeMessageRemove")}</p>
|
||||
</div>
|
||||
|
||||
@@ -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 { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip";
|
||||
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();
|
||||
@@ -36,7 +46,16 @@ export default function CredentialsPage() {
|
||||
const { remoteExitNode } = useRemoteExitNodeContext();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [credentials, setCredentials] = useState<PickRemoteExitNodeDefaultsResponse | null>(null);
|
||||
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 [shouldDisconnect, setShouldDisconnect] = useState(true);
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
@@ -48,86 +67,213 @@ export default function CredentialsPage() {
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
};
|
||||
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
try {
|
||||
const response = await api.get<
|
||||
AxiosResponse<PickRemoteExitNodeDefaultsResponse>
|
||||
>(`/org/${orgId}/pick-remote-exit-node-defaults`);
|
||||
|
||||
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,
|
||||
disconnect: shouldDisconnect
|
||||
});
|
||||
|
||||
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>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("generatedcredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("regenerateCredentials")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<>
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("generatedcredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("regenerateCredentials")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SecurityFeaturesAlert />
|
||||
|
||||
<SettingsSectionBody>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-block">
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("regeneratecredentials")}
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<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>
|
||||
|
||||
{isSecurityFeatureDisabled() && (
|
||||
<TooltipContent side="top">
|
||||
{t("featureDisabledTooltip")}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
{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>
|
||||
{build !== "oss" && (
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShouldDisconnect(false);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("regenerateCredentialsButton")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShouldDisconnect(true);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("remoteExitNodeRegenerateAndDisconnect")}
|
||||
</Button>
|
||||
</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">
|
||||
{shouldDisconnect ? (
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
"remoteExitNodeRegenerateAndDisconnectConfirmation"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
"remoteExitNodeRegenerateAndDisconnectWarning"
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
"remoteExitNodeRegenerateCredentialsConfirmation"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
"remoteExitNodeRegenerateCredentialsWarning"
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
buttonText={
|
||||
shouldDisconnect
|
||||
? t("remoteExitNodeRegenerateAndDisconnect")
|
||||
: t("regenerateCredentialsButton")
|
||||
}
|
||||
onConfirm={handleConfirmRegenerate}
|
||||
string={getConfirmationString()}
|
||||
title={t("regenerateCredentials")}
|
||||
warningText={t("cannotbeUndone")}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('credentials'),
|
||||
title: t("credentials"),
|
||||
href: "/{orgId}/settings/remote-exit-nodes/{remoteExitNodeId}/credentials"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -86,7 +86,7 @@ export default function CreateRemoteExitNodePage() {
|
||||
useEffect(() => {
|
||||
const remoteExitNodeId = searchParams.get("remoteExitNodeId");
|
||||
const remoteExitNodeSecret = searchParams.get("remoteExitNodeSecret");
|
||||
|
||||
|
||||
if (remoteExitNodeId && remoteExitNodeSecret) {
|
||||
setStrategy("adopt");
|
||||
form.setValue("remoteExitNodeId", remoteExitNodeId);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { AxiosResponse } from "axios";
|
||||
import InvitationsTable, { InvitationRow } from "../../../../../components/InvitationsTable";
|
||||
import InvitationsTable, {
|
||||
InvitationRow
|
||||
} from "../../../../../components/InvitationsTable";
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import { cache } from "react";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
@@ -9,7 +11,7 @@ import UserProvider from "@app/providers/UserProvider";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type InvitationsPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -68,7 +70,7 @@ export default async function InvitationsPage(props: InvitationsPageProps) {
|
||||
id: invite.inviteId,
|
||||
email: invite.email,
|
||||
expiresAt: new Date(Number(invite.expiresAt)).toISOString(),
|
||||
role: invite.roleName || t('accessRoleUnknown'),
|
||||
role: invite.roleName || t("accessRoleUnknown"),
|
||||
roleId: invite.roleId
|
||||
};
|
||||
});
|
||||
@@ -76,8 +78,8 @@ export default async function InvitationsPage(props: InvitationsPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('inviteTitle')}
|
||||
description={t('inviteDescription')}
|
||||
title={t("inviteTitle")}
|
||||
description={t("inviteDescription")}
|
||||
/>
|
||||
<UserProvider user={user!}>
|
||||
<OrgProvider org={org}>
|
||||
|
||||
@@ -7,7 +7,7 @@ import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { ListRolesResponse } from "@server/routers/role";
|
||||
import RolesTable, { RoleRow } from "../../../../../components/RolesTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type RolesPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -66,8 +66,8 @@ export default async function RolesPage(props: RolesPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('accessRolesManage')}
|
||||
description={t('accessRolesDescription')}
|
||||
title={t("accessRolesManage")}
|
||||
description={t("accessRolesDescription")}
|
||||
/>
|
||||
<OrgProvider org={org}>
|
||||
<RolesTable roles={roleRows} />
|
||||
|
||||
@@ -106,7 +106,8 @@ export default function Page() {
|
||||
|
||||
const genericOidcFormSchema = z.object({
|
||||
username: z.string().min(1, { message: t("usernameRequired") }),
|
||||
email: z.email({ message: t("emailInvalid") })
|
||||
email: z
|
||||
.email({ message: t("emailInvalid") })
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
name: z.string().optional(),
|
||||
|
||||
@@ -10,7 +10,7 @@ import UserProvider from "@app/providers/UserProvider";
|
||||
import { verifySession } from "@app/lib/auth/verifySession";
|
||||
import AccessPageHeaderAndNav from "../../../../../components/AccessPageHeaderAndNav";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type UsersPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -79,9 +79,11 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||
type: user.type,
|
||||
idpVariant: user.idpVariant,
|
||||
idpId: user.idpId,
|
||||
idpName: user.idpName || t('idpNameInternal'),
|
||||
status: t('userConfirmed'),
|
||||
role: user.isOwner ? t('accessRoleOwner') : user.roleName || t('accessRoleMember'),
|
||||
idpName: user.idpName || t("idpNameInternal"),
|
||||
status: t("userConfirmed"),
|
||||
role: user.isOwner
|
||||
? t("accessRoleOwner")
|
||||
: user.roleName || t("accessRoleMember"),
|
||||
isOwner: user.isOwner || false
|
||||
};
|
||||
});
|
||||
@@ -89,8 +91,8 @@ export default async function UsersPage(props: UsersPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('accessUsersManage')}
|
||||
description={t('accessUsersDescription')}
|
||||
title={t("accessUsersManage")}
|
||||
description={t("accessUsersDescription")}
|
||||
/>
|
||||
<UserProvider user={user!}>
|
||||
<OrgProvider org={org}>
|
||||
|
||||
@@ -6,7 +6,7 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
||||
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -33,14 +33,16 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('apiKeysPermissionsTitle'),
|
||||
title: t("apiKeysPermissionsTitle"),
|
||||
href: "/{orgId}/settings/api-keys/{apiKeyId}/permissions"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle title={t('apiKeysSettings', {apiKeyName: apiKey?.name})} />
|
||||
<SettingsSectionTitle
|
||||
title={t("apiKeysSettings", { apiKeyName: apiKey?.name })}
|
||||
/>
|
||||
|
||||
<ApiKeyProvider apiKey={apiKey}>
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
|
||||
@@ -4,5 +4,7 @@ export default async function ApiKeysPage(props: {
|
||||
params: Promise<{ orgId: string; apiKeyId: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
redirect(`/${params.orgId}/settings/api-keys/${params.apiKeyId}/permissions`);
|
||||
redirect(
|
||||
`/${params.orgId}/settings/api-keys/${params.apiKeyId}/permissions`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,10 +45,10 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysPermissionsErrorLoadingActions'),
|
||||
title: t("apiKeysPermissionsErrorLoadingActions"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t('apiKeysPermissionsErrorLoadingActions')
|
||||
t("apiKeysPermissionsErrorLoadingActions")
|
||||
)
|
||||
});
|
||||
});
|
||||
@@ -79,18 +79,18 @@ export default function Page() {
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(t('apiKeysErrorSetPermission'), e);
|
||||
console.error(t("apiKeysErrorSetPermission"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysErrorSetPermission'),
|
||||
title: t("apiKeysErrorSetPermission"),
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
|
||||
if (actionsRes && actionsRes.status === 200) {
|
||||
toast({
|
||||
title: t('apiKeysPermissionsUpdated'),
|
||||
description: t('apiKeysPermissionsUpdatedDescription')
|
||||
title: t("apiKeysPermissionsUpdated"),
|
||||
description: t("apiKeysPermissionsUpdatedDescription")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -104,10 +104,12 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysPermissionsGeneralSettings')}
|
||||
{t("apiKeysPermissionsGeneralSettings")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('apiKeysPermissionsGeneralSettingsDescription')}
|
||||
{t(
|
||||
"apiKeysPermissionsGeneralSettingsDescription"
|
||||
)}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -124,7 +126,7 @@ export default function Page() {
|
||||
loading={loadingSavePermissions}
|
||||
disabled={loadingSavePermissions}
|
||||
>
|
||||
{t('apiKeysPermissionsSave')}
|
||||
{t("apiKeysPermissionsSave")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSectionBody>
|
||||
|
||||
@@ -66,10 +66,10 @@ export default function Page() {
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: t('nameMin', {len: 2})
|
||||
message: t("nameMin", { len: 2 })
|
||||
})
|
||||
.max(255, {
|
||||
message: t('nameMax', {len: 255})
|
||||
message: t("nameMax", { len: 255 })
|
||||
})
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ export default function Page() {
|
||||
return data.copied;
|
||||
},
|
||||
{
|
||||
message: t('apiKeysConfirmCopy2'),
|
||||
message: t("apiKeysConfirmCopy2"),
|
||||
path: ["copied"]
|
||||
}
|
||||
);
|
||||
@@ -119,7 +119,7 @@ export default function Page() {
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysErrorCreate'),
|
||||
title: t("apiKeysErrorCreate"),
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
@@ -140,10 +140,10 @@ export default function Page() {
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(t('apiKeysErrorSetPermission'), e);
|
||||
console.error(t("apiKeysErrorSetPermission"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('apiKeysErrorSetPermission'),
|
||||
title: t("apiKeysErrorSetPermission"),
|
||||
description: formatAxiosError(e)
|
||||
});
|
||||
});
|
||||
@@ -182,8 +182,8 @@ export default function Page() {
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<HeaderTitle
|
||||
title={t('apiKeysCreate')}
|
||||
description={t('apiKeysCreateDescription')}
|
||||
title={t("apiKeysCreate")}
|
||||
description={t("apiKeysCreateDescription")}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -191,7 +191,7 @@ export default function Page() {
|
||||
router.push(`/${orgId}/settings/api-keys`);
|
||||
}}
|
||||
>
|
||||
{t('apiKeysSeeAll')}
|
||||
{t("apiKeysSeeAll")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -203,7 +203,7 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysTitle')}
|
||||
{t("apiKeysTitle")}
|
||||
</SettingsSectionTitle>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -224,7 +224,7 @@ export default function Page() {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('name')}
|
||||
{t("name")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
@@ -245,10 +245,12 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysGeneralSettings')}
|
||||
{t("apiKeysGeneralSettings")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('apiKeysGeneralSettingsDescription')}
|
||||
{t(
|
||||
"apiKeysGeneralSettingsDescription"
|
||||
)}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -267,14 +269,14 @@ export default function Page() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('apiKeysList')}
|
||||
{t("apiKeysList")}
|
||||
</SettingsSectionTitle>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<InfoSections cols={2}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t('name')}
|
||||
{t("name")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
@@ -284,7 +286,7 @@ export default function Page() {
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t('created')}
|
||||
{t("created")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{moment(
|
||||
@@ -297,10 +299,10 @@ export default function Page() {
|
||||
<Alert variant="neutral">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t('apiKeysSave')}
|
||||
{t("apiKeysSave")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('apiKeysSaveDescription')}
|
||||
{t("apiKeysSaveDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -367,7 +369,7 @@ export default function Page() {
|
||||
router.push(`/${orgId}/settings/api-keys`);
|
||||
}}
|
||||
>
|
||||
{t('cancel')}
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
)}
|
||||
{!apiKey && (
|
||||
@@ -379,7 +381,7 @@ export default function Page() {
|
||||
form.handleSubmit(onSubmit)();
|
||||
}}
|
||||
>
|
||||
{t('generate')}
|
||||
{t("generate")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -390,7 +392,7 @@ export default function Page() {
|
||||
copiedForm.handleSubmit(onCopiedSubmit)();
|
||||
}}
|
||||
>
|
||||
{t('done')}
|
||||
{t("done")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,11 @@ import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { AxiosResponse } from "axios";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import OrgApiKeysTable, { OrgApiKeyRow } from "../../../../components/OrgApiKeysTable";
|
||||
import OrgApiKeysTable, {
|
||||
OrgApiKeyRow
|
||||
} from "../../../../components/OrgApiKeysTable";
|
||||
import { ListOrgApiKeysResponse } from "@server/routers/apiKeys";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type ApiKeyPageProps = {
|
||||
params: Promise<{ orgId: string }>;
|
||||
@@ -37,8 +39,8 @@ export default async function ApiKeysPage(props: ApiKeyPageProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('apiKeysManage')}
|
||||
description={t('apiKeysDescription')}
|
||||
title={t("apiKeysManage")}
|
||||
description={t("apiKeysDescription")}
|
||||
/>
|
||||
|
||||
<OrgApiKeysTable apiKeys={rows} orgId={params.orgId} />
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger
|
||||
} from "@app/components/ui/tooltip";
|
||||
import { useClientContext } from "@app/hooks/useClientContext";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { build } from "@server/build";
|
||||
import { PickClientDefaultsResponse } from "@server/routers/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { client } = useClientContext();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [clientDefaults, setClientDefaults] =
|
||||
useState<PickClientDefaultsResponse | null>(null);
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
};
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
const res = await api.get(`/org/${orgId}/pick-client-defaults`);
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data.data;
|
||||
setClientDefaults(data);
|
||||
|
||||
await api.post(
|
||||
`/re-key/${client?.clientId}/regenerate-client-secret`,
|
||||
{
|
||||
olmId: data.olmId,
|
||||
secret: data.olmSecret
|
||||
}
|
||||
);
|
||||
|
||||
toast({
|
||||
title: t("credentialsSaved"),
|
||||
description: t("credentialsSavedDescription")
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
const getCredentials = () => {
|
||||
if (clientDefaults) {
|
||||
return {
|
||||
Id: clientDefaults.olmId,
|
||||
Secret: clientDefaults.olmSecret
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("generatedcredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("regenerateCredentials")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-block">
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("regeneratecredentials")}
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
{isSecurityFeatureDisabled() && (
|
||||
<TooltipContent side="top">
|
||||
{t("featureDisabledTooltip")}
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<RegenerateCredentialsModal
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
type="client-olm"
|
||||
onConfirmRegenerate={handleConfirmRegenerate}
|
||||
dashboardUrl={env.app.dashboardUrl}
|
||||
credentials={getCredentials()}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionFooter,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { PickClientDefaultsResponse } from "@server/routers/client";
|
||||
import { useClientContext } from "@app/hooks/useClientContext";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
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();
|
||||
const api = createApiClient({ env });
|
||||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { client } = useClientContext();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [clientDefaults, setClientDefaults] =
|
||||
useState<PickClientDefaultsResponse | null>(null);
|
||||
const [currentOlmId, setCurrentOlmId] = useState<string | null>(
|
||||
client.olmId
|
||||
);
|
||||
const [regeneratedSecret, setRegeneratedSecret] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [showCredentialsAlert, setShowCredentialsAlert] = useState(false);
|
||||
const [shouldDisconnect, setShouldDisconnect] = useState(true);
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
};
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
try {
|
||||
const res = await api.get(`/org/${orgId}/pick-client-defaults`);
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data.data;
|
||||
|
||||
const rekeyRes = await api.post(
|
||||
`/re-key/${client?.clientId}/regenerate-client-secret`,
|
||||
{
|
||||
secret: data.olmSecret,
|
||||
disconnect: shouldDisconnect
|
||||
}
|
||||
);
|
||||
|
||||
if (rekeyRes && rekeyRes.status === 200) {
|
||||
const rekeyData = rekeyRes.data.data;
|
||||
if (rekeyData && rekeyData.olmId) {
|
||||
setCurrentOlmId(rekeyData.olmId);
|
||||
setRegeneratedSecret(data.olmSecret);
|
||||
setClientDefaults({
|
||||
...data,
|
||||
olmId: rekeyData.olmId
|
||||
});
|
||||
setShowCredentialsAlert(true);
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getConfirmationString = () => {
|
||||
return client?.name || client?.clientId?.toString() || "My client";
|
||||
};
|
||||
|
||||
const displayOlmId = currentOlmId || clientDefaults?.olmId || null;
|
||||
const displaySecret = regeneratedSecret || null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("clientOlmCredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("clientOlmCredentialsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SecurityFeaturesAlert />
|
||||
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("olmEndpoint")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
text={env.app.dashboardUrl}
|
||||
/>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("olmId")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{displayOlmId ? (
|
||||
<CopyToClipboard text={displayOlmId} />
|
||||
) : (
|
||||
<span>{"••••••••••••••••"}</span>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("olmSecretKey")}
|
||||
</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("clientCredentialsSave")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("clientCredentialsSaveDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
{build !== "oss" && (
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShouldDisconnect(false);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("regenerateCredentialsButton")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShouldDisconnect(true);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("clientRegenerateAndDisconnect")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
)}
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
|
||||
<ConfirmDeleteDialog
|
||||
open={modalOpen}
|
||||
setOpen={(val) => {
|
||||
setModalOpen(val);
|
||||
// Prevent modal from reopening during refresh
|
||||
if (!val) {
|
||||
setTimeout(() => {
|
||||
router.refresh();
|
||||
}, 150);
|
||||
}
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
{shouldDisconnect ? (
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
"clientRegenerateAndDisconnectConfirmation"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t("clientRegenerateAndDisconnectWarning")}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
"clientRegenerateCredentialsConfirmation"
|
||||
)}
|
||||
</p>
|
||||
<p>{t("clientRegenerateCredentialsWarning")}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
buttonText={
|
||||
shouldDisconnect
|
||||
? t("clientRegenerateAndDisconnect")
|
||||
: t("regenerateCredentialsButton")
|
||||
}
|
||||
onConfirm={handleConfirmRegenerate}
|
||||
string={getConfirmationString()}
|
||||
title={t("regenerateCredentials")}
|
||||
warningText={t("cannotbeUndone")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,8 @@ import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().nonempty("Name is required")
|
||||
name: z.string().nonempty("Name is required"),
|
||||
niceId: z.string().min(1).max(255).optional()
|
||||
});
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
@@ -49,7 +50,8 @@ export default function GeneralPage() {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
defaultValues: {
|
||||
name: client?.name
|
||||
name: client?.name,
|
||||
niceId: client?.niceId || ""
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
@@ -84,10 +86,11 @@ export default function GeneralPage() {
|
||||
|
||||
try {
|
||||
await api.post(`/client/${client?.clientId}`, {
|
||||
name: data.name
|
||||
name: data.name,
|
||||
niceId: data.niceId
|
||||
});
|
||||
|
||||
updateClient({ name: data.name });
|
||||
updateClient({ name: data.name, niceId: data.niceId });
|
||||
|
||||
toast({
|
||||
title: t("clientUpdated"),
|
||||
@@ -139,6 +142,28 @@ export default function GeneralPage() {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("identifier")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t(
|
||||
"enterIdentifier"
|
||||
)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
@@ -4,7 +4,6 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import ClientProvider from "@app/providers/ClientProvider";
|
||||
import { build } from "@server/build";
|
||||
import { GetClientResponse } from "@server/routers/client";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
@@ -12,7 +11,7 @@ import { redirect } from "next/navigation";
|
||||
|
||||
type SettingsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ clientId: number | string; orgId: string }>;
|
||||
params: Promise<{ niceId: number | string; orgId: string }>;
|
||||
};
|
||||
|
||||
export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
@@ -22,8 +21,12 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
let client = null;
|
||||
try {
|
||||
console.log(
|
||||
"making request to ",
|
||||
`/org/${params.orgId}/client/${params.niceId}`
|
||||
);
|
||||
const res = await internal.get<AxiosResponse<GetClientResponse>>(
|
||||
`/client/${params.clientId}`,
|
||||
`/org/${params.orgId}/client/${params.niceId}`,
|
||||
await authCookieHeader()
|
||||
);
|
||||
client = res.data.data;
|
||||
@@ -37,16 +40,12 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
const navItems = [
|
||||
{
|
||||
title: t("general"),
|
||||
href: `/{orgId}/settings/clients/machine/{clientId}/general`
|
||||
href: `/{orgId}/settings/clients/machine/{niceId}/general`
|
||||
},
|
||||
...(build === "enterprise"
|
||||
? [
|
||||
{
|
||||
title: t("credentials"),
|
||||
href: `/{orgId}/settings/clients/machine/{clientId}/credentials`
|
||||
}
|
||||
]
|
||||
: [])
|
||||
{
|
||||
title: t("credentials"),
|
||||
href: `/{orgId}/settings/clients/machine/{niceId}/credentials`
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -1,10 +1,10 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function ClientPage(props: {
|
||||
params: Promise<{ orgId: string; clientId: number | string }>;
|
||||
params: Promise<{ orgId: string; niceId: number | string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
redirect(
|
||||
`/${params.orgId}/settings/clients/machine/${params.clientId}/general`
|
||||
`/${params.orgId}/settings/clients/machine/${params.niceId}/general`
|
||||
);
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export default function Page() {
|
||||
All: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -fsSL https://pangolin.net/get-olm.sh | bash`
|
||||
command: `curl -fsSL https://static.pangolin.net/get-olm.sh | bash`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
@@ -276,7 +276,7 @@ export default function Page() {
|
||||
|
||||
if (res && res.status === 201) {
|
||||
const data = res.data.data;
|
||||
router.push(`/${orgId}/settings/clients/machine/${data.clientId}`);
|
||||
router.push(`/${orgId}/settings/clients/machine/${data.niceId}`);
|
||||
}
|
||||
|
||||
setCreateLoading(false);
|
||||
|
||||
@@ -56,7 +56,9 @@ export default async function ClientsPage(props: ClientsPageProps) {
|
||||
olmUpdateAvailable: client.olmUpdateAvailable || false,
|
||||
userId: client.userId,
|
||||
username: client.username,
|
||||
userEmail: client.userEmail
|
||||
userEmail: client.userEmail,
|
||||
niceId: client.niceId,
|
||||
agent: client.agent
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ export default async function ClientsPage(props: ClientsPageProps) {
|
||||
olmUpdateAvailable: client.olmUpdateAvailable || false,
|
||||
userId: client.userId,
|
||||
username: client.username,
|
||||
userEmail: client.userEmail
|
||||
userEmail: client.userEmail,
|
||||
niceId: client.niceId,
|
||||
agent: client.agent
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import OrgProvider from "@app/providers/OrgProvider";
|
||||
import OrgUserProvider from "@app/providers/OrgUserProvider";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getCachedOrg } from "@app/lib/api/getCachedOrg";
|
||||
import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser";
|
||||
|
||||
@@ -102,7 +102,12 @@ const LOG_RETENTION_OPTIONS = [
|
||||
{ label: "logRetention14Days", value: 14 },
|
||||
{ label: "logRetention30Days", value: 30 },
|
||||
{ label: "logRetention90Days", value: 90 },
|
||||
...(build !== "saas" ? [{ label: "logRetentionForever", value: -1 }] : [])
|
||||
...(build != "saas"
|
||||
? [
|
||||
{ label: "logRetentionForever", value: -1 },
|
||||
{ label: "logRetentionEndOfFollowingYear", value: 9001 }
|
||||
]
|
||||
: [])
|
||||
];
|
||||
|
||||
export default function GeneralPage() {
|
||||
@@ -265,7 +270,7 @@ export default function GeneralPage() {
|
||||
setIsDeleteModalOpen(val);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("orgQuestionRemove")}</p>
|
||||
<p>{t("orgMessageRemove")}</p>
|
||||
</div>
|
||||
@@ -279,7 +284,7 @@ export default function GeneralPage() {
|
||||
open={isSecurityPolicyConfirmOpen}
|
||||
setOpen={setIsSecurityPolicyConfirmOpen}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("securityPolicyChangeDescription")}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
"use client";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { useState, useRef, useEffect, useTransition } from "react";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
getStoredPageSize,
|
||||
LogDataTable,
|
||||
setStoredPageSize
|
||||
} from "@app/components/LogDataTable";
|
||||
import { LogDataTable } from "@app/components/LogDataTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { ArrowUpRight, Key, User } from "lucide-react";
|
||||
@@ -21,21 +17,22 @@ import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusCo
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import axios from "axios";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { orgId } = useParams();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [isExporting, startTransition] = useTransition();
|
||||
const [filterAttributes, setFilterAttributes] = useState<{
|
||||
actors: string[];
|
||||
resources: {
|
||||
@@ -70,9 +67,7 @@ export default function GeneralPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Initialize page size from storage or default
|
||||
const [pageSize, setPageSize] = useState<number>(() => {
|
||||
return getStoredPageSize("access-audit-logs", 20);
|
||||
});
|
||||
const [pageSize, setPageSize] = useStoredPageSize("access-audit-logs", 20);
|
||||
|
||||
// Set default date range to last 24 hours
|
||||
const getDefaultDateRange = () => {
|
||||
@@ -91,11 +86,11 @@ export default function GeneralPage() {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const lastWeek = getSevenDaysAgo();
|
||||
|
||||
return {
|
||||
startDate: {
|
||||
date: yesterday
|
||||
date: lastWeek
|
||||
},
|
||||
endDate: {
|
||||
date: now
|
||||
@@ -148,7 +143,6 @@ export default function GeneralPage() {
|
||||
// Handle page size changes
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
setPageSize(newPageSize);
|
||||
setStoredPageSize(newPageSize, "access-audit-logs");
|
||||
setCurrentPage(0); // Reset to first page when changing page size
|
||||
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||
};
|
||||
@@ -309,8 +303,6 @@ export default function GeneralPage() {
|
||||
|
||||
const exportData = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
// Prepare query params for export
|
||||
const params: any = {
|
||||
timeStart: dateRange.startDate?.date
|
||||
@@ -339,11 +331,21 @@ export default function GeneralPage() {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode?.removeChild(link);
|
||||
setIsExporting(false);
|
||||
} catch (error) {
|
||||
let apiErrorMessage: string | null = null;
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const data = error.response.data;
|
||||
|
||||
if (data instanceof Blob && data.type === "application/json") {
|
||||
// Parse the Blob as JSON
|
||||
const text = await data.text();
|
||||
const errorData = JSON.parse(text);
|
||||
apiErrorMessage = errorData.message;
|
||||
}
|
||||
}
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("exportError"),
|
||||
description: apiErrorMessage ?? t("exportError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
@@ -631,7 +633,7 @@ export default function GeneralPage() {
|
||||
title={t("accessLogs")}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
onExport={exportData}
|
||||
onExport={() => startTransition(exportData)}
|
||||
isExporting={isExporting}
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
dateRange={{
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
"use client";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
getStoredPageSize,
|
||||
LogDataTable,
|
||||
setStoredPageSize
|
||||
} from "@app/components/LogDataTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { Key, User } from "lucide-react";
|
||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { LogDataTable } from "@app/components/LogDataTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import { build } from "@server/build";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import { Key, User } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const router = useRouter();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { orgId } = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
@@ -34,7 +30,7 @@ export default function GeneralPage() {
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [isExporting, startTransition] = useTransition();
|
||||
const [filterAttributes, setFilterAttributes] = useState<{
|
||||
actors: string[];
|
||||
actions: string[];
|
||||
@@ -58,9 +54,7 @@ export default function GeneralPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Initialize page size from storage or default
|
||||
const [pageSize, setPageSize] = useState<number>(() => {
|
||||
return getStoredPageSize("action-audit-logs", 20);
|
||||
});
|
||||
const [pageSize, setPageSize] = useStoredPageSize("action-audit-logs", 20);
|
||||
|
||||
// Set default date range to last 24 hours
|
||||
const getDefaultDateRange = () => {
|
||||
@@ -79,11 +73,11 @@ export default function GeneralPage() {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const lastWeek = getSevenDaysAgo();
|
||||
|
||||
return {
|
||||
startDate: {
|
||||
date: yesterday
|
||||
date: lastWeek
|
||||
},
|
||||
endDate: {
|
||||
date: now
|
||||
@@ -136,7 +130,6 @@ export default function GeneralPage() {
|
||||
// Handle page size changes
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
setPageSize(newPageSize);
|
||||
setStoredPageSize(newPageSize, "action-audit-logs");
|
||||
setCurrentPage(0); // Reset to first page when changing page size
|
||||
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||
};
|
||||
@@ -293,8 +286,6 @@ export default function GeneralPage() {
|
||||
|
||||
const exportData = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
// Prepare query params for export
|
||||
const params: any = {
|
||||
timeStart: dateRange.startDate?.date
|
||||
@@ -323,11 +314,21 @@ export default function GeneralPage() {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode?.removeChild(link);
|
||||
setIsExporting(false);
|
||||
} catch (error) {
|
||||
let apiErrorMessage: string | null = null;
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const data = error.response.data;
|
||||
|
||||
if (data instanceof Blob && data.type === "application/json") {
|
||||
// Parse the Blob as JSON
|
||||
const text = await data.text();
|
||||
const errorData = JSON.parse(text);
|
||||
apiErrorMessage = errorData.message;
|
||||
}
|
||||
}
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("exportError"),
|
||||
description: apiErrorMessage ?? t("exportError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
@@ -484,7 +485,7 @@ export default function GeneralPage() {
|
||||
searchColumn="action"
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
onExport={exportData}
|
||||
onExport={() => startTransition(exportData)}
|
||||
isExporting={isExporting}
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
dateRange={{
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
"use client";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getStoredPageSize, LogDataTable, setStoredPageSize } from "@app/components/LogDataTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { Key, RouteOff, User, Lock, Unlock, ArrowUpRight } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { LogDataTable } from "@app/components/LogDataTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import axios from "axios";
|
||||
import { ArrowUpRight, Key, Lock, Unlock, User } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const router = useRouter();
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { orgId } = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [isExporting, startTransition] = useTransition();
|
||||
|
||||
// Pagination state
|
||||
const [totalCount, setTotalCount] = useState<number>(0);
|
||||
@@ -32,9 +34,7 @@ export default function GeneralPage() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Initialize page size from storage or default
|
||||
const [pageSize, setPageSize] = useState<number>(() => {
|
||||
return getStoredPageSize("request-audit-logs", 20);
|
||||
});
|
||||
const [pageSize, setPageSize] = useStoredPageSize("request-audit-logs", 20);
|
||||
|
||||
const [filterAttributes, setFilterAttributes] = useState<{
|
||||
actors: string[];
|
||||
@@ -91,11 +91,11 @@ export default function GeneralPage() {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
const lastWeek = getSevenDaysAgo();
|
||||
|
||||
return {
|
||||
startDate: {
|
||||
date: yesterday
|
||||
date: lastWeek
|
||||
},
|
||||
endDate: {
|
||||
date: now
|
||||
@@ -148,7 +148,6 @@ export default function GeneralPage() {
|
||||
// Handle page size changes
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
setPageSize(newPageSize);
|
||||
setStoredPageSize(newPageSize, "request-audit-logs");
|
||||
setCurrentPage(0); // Reset to first page when changing page size
|
||||
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||
};
|
||||
@@ -298,8 +297,6 @@ export default function GeneralPage() {
|
||||
|
||||
const exportData = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
|
||||
// Prepare query params for export
|
||||
const params: any = {
|
||||
timeStart: dateRange.startDate?.date
|
||||
@@ -331,11 +328,21 @@ export default function GeneralPage() {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.parentNode?.removeChild(link);
|
||||
setIsExporting(false);
|
||||
} catch (error) {
|
||||
let apiErrorMessage: string | null = null;
|
||||
if (axios.isAxiosError(error) && error.response) {
|
||||
const data = error.response.data;
|
||||
|
||||
if (data instanceof Blob && data.type === "application/json") {
|
||||
// Parse the Blob as JSON
|
||||
const text = await data.text();
|
||||
const errorData = JSON.parse(text);
|
||||
apiErrorMessage = errorData.message;
|
||||
}
|
||||
}
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("exportError"),
|
||||
description: apiErrorMessage ?? t("exportError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
@@ -757,8 +764,8 @@ export default function GeneralPage() {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('requestLogs')}
|
||||
description={t('requestLogsDescription')}
|
||||
title={t("requestLogs")}
|
||||
description={t("requestLogsDescription")}
|
||||
/>
|
||||
|
||||
<LogDataTable
|
||||
@@ -769,7 +776,7 @@ export default function GeneralPage() {
|
||||
searchColumn="host"
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
onExport={exportData}
|
||||
onExport={() => startTransition(exportData)}
|
||||
isExporting={isExporting}
|
||||
onDateRangeChange={handleDateRangeChange}
|
||||
dateRange={{
|
||||
|
||||
@@ -66,7 +66,11 @@ export default async function ClientResourcesPage(
|
||||
destination: siteResource.destination,
|
||||
// destinationPort: siteResource.destinationPort,
|
||||
alias: siteResource.alias || null,
|
||||
siteNiceId: siteResource.siteNiceId
|
||||
siteNiceId: siteResource.siteNiceId,
|
||||
niceId: siteResource.niceId,
|
||||
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
||||
udpPortRangeString: siteResource.udpPortRangeString || null,
|
||||
disableIcmp: siteResource.disableIcmp || false,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -837,7 +837,9 @@ export default function ResourceAuthenticationPage() {
|
||||
<Bot size="14" />
|
||||
<span>
|
||||
{authInfo.headerAuth
|
||||
? t("resourceHeaderAuthProtectionEnabled")
|
||||
? t(
|
||||
"resourceHeaderAuthProtectionEnabled"
|
||||
)
|
||||
: t(
|
||||
"resourceHeaderAuthProtectionDisabled"
|
||||
)}
|
||||
@@ -921,7 +923,8 @@ export default function ResourceAuthenticationPage() {
|
||||
validateTag={(
|
||||
tag
|
||||
) => {
|
||||
return z.email()
|
||||
return z
|
||||
.email()
|
||||
.or(
|
||||
z
|
||||
.string()
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||
import { ListSitesResponse } from "@server/routers/site";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -90,8 +90,15 @@ export default function GeneralForm() {
|
||||
const [resourceFullDomain, setResourceFullDomain] = useState(
|
||||
`${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}`
|
||||
);
|
||||
|
||||
const resourceFullDomainName = useMemo(() => {
|
||||
const url = new URL(resourceFullDomain);
|
||||
return url.hostname;
|
||||
}, [resourceFullDomain]);
|
||||
|
||||
const [selectedDomain, setSelectedDomain] = useState<{
|
||||
domainId: string;
|
||||
domainNamespaceId?: string;
|
||||
subdomain?: string;
|
||||
fullDomain: string;
|
||||
baseDomain: string;
|
||||
@@ -104,7 +111,7 @@ export default function GeneralForm() {
|
||||
name: z.string().min(1).max(255),
|
||||
niceId: z.string().min(1).max(255).optional(),
|
||||
domainId: z.string().optional(),
|
||||
proxyPort: z.int().min(1).max(65535).optional(),
|
||||
proxyPort: z.int().min(1).max(65535).optional()
|
||||
// enableProxy: z.boolean().optional()
|
||||
})
|
||||
.refine(
|
||||
@@ -134,7 +141,7 @@ export default function GeneralForm() {
|
||||
niceId: resource.niceId,
|
||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||
domainId: resource.domainId || undefined,
|
||||
proxyPort: resource.proxyPort || undefined,
|
||||
proxyPort: resource.proxyPort || undefined
|
||||
// enableProxy: resource.enableProxy || false
|
||||
},
|
||||
mode: "onChange"
|
||||
@@ -168,7 +175,7 @@ export default function GeneralForm() {
|
||||
const rawDomains = res.data.data.domains as DomainRow[];
|
||||
const domains = rawDomains.map((domain) => ({
|
||||
...domain,
|
||||
baseDomain: toUnicode(domain.baseDomain),
|
||||
baseDomain: toUnicode(domain.baseDomain)
|
||||
}));
|
||||
setBaseDomains(domains);
|
||||
setFormKey((key) => key + 1);
|
||||
@@ -195,9 +202,11 @@ export default function GeneralForm() {
|
||||
enabled: data.enabled,
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
subdomain: data.subdomain ? toASCII(data.subdomain) : undefined,
|
||||
subdomain: data.subdomain
|
||||
? toASCII(data.subdomain)
|
||||
: undefined,
|
||||
domainId: data.domainId,
|
||||
proxyPort: data.proxyPort,
|
||||
proxyPort: data.proxyPort
|
||||
// ...(!resource.http && {
|
||||
// enableProxy: data.enableProxy
|
||||
// })
|
||||
@@ -222,8 +231,9 @@ export default function GeneralForm() {
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
subdomain: data.subdomain,
|
||||
fullDomain: resource.fullDomain,
|
||||
fullDomain: updated.fullDomain,
|
||||
proxyPort: data.proxyPort,
|
||||
domainId: data.domainId
|
||||
// ...(!resource.http && {
|
||||
// enableProxy: data.enableProxy
|
||||
// })
|
||||
@@ -235,7 +245,9 @@ export default function GeneralForm() {
|
||||
});
|
||||
|
||||
if (data.niceId && data.niceId !== resource?.niceId) {
|
||||
router.replace(`/${updated.orgId}/settings/resources/proxy/${data.niceId}/general`);
|
||||
router.replace(
|
||||
`/${updated.orgId}/settings/resources/proxy/${data.niceId}/general`
|
||||
);
|
||||
} else {
|
||||
router.refresh();
|
||||
}
|
||||
@@ -320,11 +332,15 @@ export default function GeneralForm() {
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("identifier")}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("identifier")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t("enterIdentifier")}
|
||||
placeholder={t(
|
||||
"enterIdentifier"
|
||||
)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
@@ -360,10 +376,10 @@ export default function GeneralForm() {
|
||||
.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
@@ -480,13 +496,27 @@ export default function GeneralForm() {
|
||||
<DomainPicker
|
||||
orgId={orgId as string}
|
||||
cols={1}
|
||||
defaultSubdomain={
|
||||
form.getValues("subdomain") ??
|
||||
resource.subdomain
|
||||
}
|
||||
defaultDomainId={
|
||||
form.getValues("domainId") ??
|
||||
resource.domainId
|
||||
}
|
||||
defaultFullDomain={resourceFullDomainName}
|
||||
onDomainChange={(res) => {
|
||||
const selected = {
|
||||
domainId: res.domainId,
|
||||
subdomain: res.subdomain,
|
||||
fullDomain: res.fullDomain,
|
||||
baseDomain: res.baseDomain
|
||||
};
|
||||
const selected =
|
||||
res === null
|
||||
? null
|
||||
: {
|
||||
domainId: res.domainId,
|
||||
subdomain: res.subdomain,
|
||||
fullDomain: res.fullDomain,
|
||||
baseDomain: res.baseDomain,
|
||||
domainNamespaceId:
|
||||
res.domainNamespaceId
|
||||
};
|
||||
setSelectedDomain(selected);
|
||||
}}
|
||||
/>
|
||||
@@ -498,17 +528,29 @@ export default function GeneralForm() {
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (selectedDomain) {
|
||||
const sanitizedSubdomain = selectedDomain.subdomain
|
||||
? finalizeSubdomainSanitize(selectedDomain.subdomain)
|
||||
: "";
|
||||
const sanitizedSubdomain =
|
||||
selectedDomain.subdomain
|
||||
? finalizeSubdomainSanitize(
|
||||
selectedDomain.subdomain
|
||||
)
|
||||
: "";
|
||||
|
||||
const sanitizedFullDomain = sanitizedSubdomain
|
||||
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
|
||||
: selectedDomain.baseDomain;
|
||||
const sanitizedFullDomain =
|
||||
sanitizedSubdomain
|
||||
? `${sanitizedSubdomain}.${selectedDomain.baseDomain}`
|
||||
: selectedDomain.baseDomain;
|
||||
|
||||
setResourceFullDomain(`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`);
|
||||
form.setValue("domainId", selectedDomain.domainId);
|
||||
form.setValue("subdomain", sanitizedSubdomain);
|
||||
setResourceFullDomain(
|
||||
`${resource.ssl ? "https" : "http"}://${sanitizedFullDomain}`
|
||||
);
|
||||
form.setValue(
|
||||
"domainId",
|
||||
selectedDomain.domainId
|
||||
);
|
||||
form.setValue(
|
||||
"subdomain",
|
||||
sanitizedSubdomain
|
||||
);
|
||||
|
||||
setEditDomainOpen(false);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { cache } from "react";
|
||||
import ResourceInfoBox from "@app/components/ResourceInfoBox";
|
||||
import { GetSiteResponse } from "@server/routers/site";
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
interface ResourceLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -76,22 +76,22 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('general'),
|
||||
title: t("general"),
|
||||
href: `/{orgId}/settings/resources/proxy/{niceId}/general`
|
||||
},
|
||||
{
|
||||
title: t('proxy'),
|
||||
title: t("proxy"),
|
||||
href: `/{orgId}/settings/resources/proxy/{niceId}/proxy`
|
||||
}
|
||||
];
|
||||
|
||||
if (resource.http) {
|
||||
navItems.push({
|
||||
title: t('authentication'),
|
||||
title: t("authentication"),
|
||||
href: `/{orgId}/settings/resources/proxy/{niceId}/authentication`
|
||||
});
|
||||
navItems.push({
|
||||
title: t('rules'),
|
||||
title: t("rules"),
|
||||
href: `/{orgId}/settings/resources/proxy/{niceId}/rules`
|
||||
});
|
||||
}
|
||||
@@ -99,15 +99,12 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('resourceSetting', {resourceName: resource?.name})}
|
||||
description={t('resourceSettingDescription')}
|
||||
title={t("resourceSetting", { resourceName: resource?.name })}
|
||||
description={t("resourceSettingDescription")}
|
||||
/>
|
||||
|
||||
<OrgProvider org={org}>
|
||||
<ResourceProvider
|
||||
resource={resource}
|
||||
authInfo={authInfo}
|
||||
>
|
||||
<ResourceProvider resource={resource} authInfo={authInfo}>
|
||||
<div className="space-y-6">
|
||||
<ResourceInfoBox />
|
||||
<HorizontalTabs items={navItems}>
|
||||
|
||||
@@ -123,10 +123,9 @@ const addTargetSchema = z
|
||||
ip: z.string().refine(isTargetValid),
|
||||
method: z.string().nullable(),
|
||||
port: z.coerce.number<number>().int().positive(),
|
||||
siteId: z.int()
|
||||
.positive({
|
||||
error: "You must select a site for a target."
|
||||
}),
|
||||
siteId: z.int().positive({
|
||||
error: "You must select a site for a target."
|
||||
}),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z
|
||||
.enum(["exact", "prefix", "regex"])
|
||||
@@ -179,8 +178,12 @@ const addTargetSchema = z
|
||||
return false;
|
||||
}
|
||||
// If rewritePathType is provided, rewritePath must be provided
|
||||
// Exception: stripPrefix can have an empty rewritePath (to just strip the prefix)
|
||||
if (data.rewritePathType && !data.rewritePath) {
|
||||
return false;
|
||||
// Allow empty rewritePath for stripPrefix type
|
||||
if (data.rewritePathType !== "stripPrefix") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -546,11 +549,11 @@ export default function ReverseProxyTargets(props: {
|
||||
prev.map((t) =>
|
||||
t.targetId === target.targetId
|
||||
? {
|
||||
...t,
|
||||
targetId: response.data.data.targetId,
|
||||
new: false,
|
||||
updated: false
|
||||
}
|
||||
...t,
|
||||
targetId: response.data.data.targetId,
|
||||
new: false,
|
||||
updated: false
|
||||
}
|
||||
: t
|
||||
)
|
||||
);
|
||||
@@ -607,16 +610,16 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const newTarget: LocalTarget = {
|
||||
...data,
|
||||
path: isHttp ? (data.path || null) : null,
|
||||
pathMatchType: isHttp ? (data.pathMatchType || null) : null,
|
||||
rewritePath: isHttp ? (data.rewritePath || null) : null,
|
||||
rewritePathType: isHttp ? (data.rewritePathType || null) : null,
|
||||
path: isHttp ? data.path || null : null,
|
||||
pathMatchType: isHttp ? data.pathMatchType || null : null,
|
||||
rewritePath: isHttp ? data.rewritePath || null : null,
|
||||
rewritePathType: isHttp ? data.rewritePathType || null : null,
|
||||
siteType: site?.type || null,
|
||||
enabled: true,
|
||||
targetId: new Date().getTime(),
|
||||
new: true,
|
||||
resourceId: resource.resourceId,
|
||||
priority: isHttp ? (data.priority || 100) : 100,
|
||||
priority: isHttp ? data.priority || 100 : 100,
|
||||
hcEnabled: false,
|
||||
hcPath: null,
|
||||
hcMethod: null,
|
||||
@@ -631,7 +634,7 @@ export default function ReverseProxyTargets(props: {
|
||||
hcStatus: null,
|
||||
hcMode: null,
|
||||
hcUnhealthyInterval: null,
|
||||
hcTlsServerName: null,
|
||||
hcTlsServerName: null
|
||||
};
|
||||
|
||||
setTargets([...targets, newTarget]);
|
||||
@@ -653,11 +656,11 @@ export default function ReverseProxyTargets(props: {
|
||||
targets.map((target) =>
|
||||
target.targetId === targetId
|
||||
? {
|
||||
...target,
|
||||
...data,
|
||||
updated: true,
|
||||
siteType: site ? site.type : target.siteType
|
||||
}
|
||||
...target,
|
||||
...data,
|
||||
updated: true,
|
||||
siteType: site ? site.type : target.siteType
|
||||
}
|
||||
: target
|
||||
)
|
||||
);
|
||||
@@ -668,10 +671,10 @@ export default function ReverseProxyTargets(props: {
|
||||
targets.map((target) =>
|
||||
target.targetId === targetId
|
||||
? {
|
||||
...target,
|
||||
...config,
|
||||
updated: true
|
||||
}
|
||||
...target,
|
||||
...config,
|
||||
updated: true
|
||||
}
|
||||
: target
|
||||
)
|
||||
);
|
||||
@@ -733,7 +736,7 @@ export default function ReverseProxyTargets(props: {
|
||||
hcStatus: target.hcStatus || null,
|
||||
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
||||
hcMode: target.hcMode || null,
|
||||
hcTlsServerName: target.hcTlsServerName,
|
||||
hcTlsServerName: target.hcTlsServerName
|
||||
};
|
||||
|
||||
// Only include path-related fields for HTTP resources
|
||||
@@ -833,7 +836,7 @@ export default function ReverseProxyTargets(props: {
|
||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||
id: "priority",
|
||||
header: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
{t("priority")}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -877,7 +880,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const healthCheckColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "healthCheck",
|
||||
header: () => (<span className="p-3">{t("healthCheck")}</span>),
|
||||
header: () => <span className="p-3">{t("healthCheck")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.hcHealth || "unknown";
|
||||
const isEnabled = row.original.hcEnabled;
|
||||
@@ -923,18 +926,17 @@ export default function ReverseProxyTargets(props: {
|
||||
{row.original.siteType === "newt" ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center justify-between gap-2 p-2 w-full text-left cursor-pointer"
|
||||
className="flex items-center gap-2 w-full text-left cursor-pointer"
|
||||
onClick={() =>
|
||||
openHealthCheckDialog(row.original)
|
||||
}
|
||||
>
|
||||
<Badge variant={getStatusColor(status)}>
|
||||
<div className="flex items-center gap-1">
|
||||
{getStatusIcon(status)}
|
||||
{getStatusText(status)}
|
||||
</div>
|
||||
</Badge>
|
||||
<Settings className="h-4 w-4" />
|
||||
<div
|
||||
className={`flex items-center gap-1 ${status === "healthy" ? "text-green-500" : status === "unhealthy" ? "text-destructive" : ""}`}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
{getStatusText(status)}
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
<span>-</span>
|
||||
@@ -949,7 +951,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const matchPathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "path",
|
||||
header: () => (<span className="p-3">{t("matchPath")}</span>),
|
||||
header: () => <span className="p-3">{t("matchPath")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const hasPathMatch = !!(
|
||||
row.original.path || row.original.pathMatchType
|
||||
@@ -1011,7 +1013,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const addressColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "address",
|
||||
header: () => (<span className="p-3">{t("address")}</span>),
|
||||
header: () => <span className="p-3">{t("address")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const selectedSite = sites.find(
|
||||
(site) => site.siteId === row.original.siteId
|
||||
@@ -1064,7 +1066,7 @@ export default function ReverseProxyTargets(props: {
|
||||
className={cn(
|
||||
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
||||
!row.original.siteId &&
|
||||
"text-muted-foreground"
|
||||
"text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
<span className="truncate max-w-[150px]">
|
||||
@@ -1132,8 +1134,12 @@ export default function ReverseProxyTargets(props: {
|
||||
{row.original.method || "http"}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="http">http</SelectItem>
|
||||
<SelectItem value="https">https</SelectItem>
|
||||
<SelectItem value="http">
|
||||
http
|
||||
</SelectItem>
|
||||
<SelectItem value="https">
|
||||
https
|
||||
</SelectItem>
|
||||
<SelectItem value="h2c">h2c</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -1147,7 +1153,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
<Input
|
||||
defaultValue={row.original.ip}
|
||||
placeholder="IP / Hostname"
|
||||
placeholder="Host"
|
||||
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
|
||||
onBlur={(e) => {
|
||||
const input = e.target.value.trim();
|
||||
@@ -1225,7 +1231,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const rewritePathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "rewritePath",
|
||||
header: () => (<span className="p-3">{t("rewritePath")}</span>),
|
||||
header: () => <span className="p-3">{t("rewritePath")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const hasRewritePath = !!(
|
||||
row.original.rewritePath || row.original.rewritePathType
|
||||
@@ -1295,7 +1301,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const enabledColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "enabled",
|
||||
header: () => (<span className="p-3">{t("enabled")}</span>),
|
||||
header: () => <span className="p-3">{t("enabled")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<Switch
|
||||
@@ -1316,7 +1322,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const actionsColumn: ColumnDef<LocalTarget> = {
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
header: () => <span className="p-3">{t("actions")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center w-full">
|
||||
<Button
|
||||
@@ -1399,21 +1405,30 @@ export default function ReverseProxyTargets(props: {
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map(
|
||||
(header) => {
|
||||
const isActionsColumn = header.column.id === "actions";
|
||||
const isActionsColumn =
|
||||
header.column
|
||||
.id ===
|
||||
"actions";
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
|
||||
key={
|
||||
header.id
|
||||
}
|
||||
className={
|
||||
isActionsColumn
|
||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
}
|
||||
@@ -1430,13 +1445,20 @@ export default function ReverseProxyTargets(props: {
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell) => {
|
||||
const isActionsColumn = cell.column.id === "actions";
|
||||
const isActionsColumn =
|
||||
cell.column
|
||||
.id ===
|
||||
"actions";
|
||||
return (
|
||||
<TableCell
|
||||
key={
|
||||
cell.id
|
||||
}
|
||||
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
|
||||
className={
|
||||
isActionsColumn
|
||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell
|
||||
@@ -1492,7 +1514,7 @@ export default function ReverseProxyTargets(props: {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-8 border-2 border-dashed border-muted rounded-lg p-4">
|
||||
<div className="text-center p-4">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{t("targetNoOne")}
|
||||
</p>
|
||||
@@ -1721,7 +1743,9 @@ export default function ReverseProxyTargets(props: {
|
||||
defaultChecked={
|
||||
field.value || false
|
||||
}
|
||||
onCheckedChange={(val) => {
|
||||
onCheckedChange={(
|
||||
val
|
||||
) => {
|
||||
field.onChange(val);
|
||||
}}
|
||||
/>
|
||||
@@ -1730,19 +1754,37 @@ export default function ReverseProxyTargets(props: {
|
||||
)}
|
||||
/>
|
||||
|
||||
{proxySettingsForm.watch("proxyProtocol") && (
|
||||
{proxySettingsForm.watch(
|
||||
"proxyProtocol"
|
||||
) && (
|
||||
<>
|
||||
<FormField
|
||||
control={proxySettingsForm.control}
|
||||
control={
|
||||
proxySettingsForm.control
|
||||
}
|
||||
name="proxyProtocolVersion"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("proxyProtocolVersion")}</FormLabel>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"proxyProtocolVersion"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={String(field.value || 1)}
|
||||
onValueChange={(value) =>
|
||||
field.onChange(parseInt(value, 10))
|
||||
value={String(
|
||||
field.value ||
|
||||
1
|
||||
)}
|
||||
onValueChange={(
|
||||
value
|
||||
) =>
|
||||
field.onChange(
|
||||
parseInt(
|
||||
value,
|
||||
10
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@@ -1750,16 +1792,22 @@ export default function ReverseProxyTargets(props: {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">
|
||||
{t("version1")}
|
||||
{t(
|
||||
"version1"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="2">
|
||||
{t("version2")}
|
||||
{t(
|
||||
"version2"
|
||||
)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("versionDescription")}
|
||||
{t(
|
||||
"versionDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -1768,7 +1816,10 @@ export default function ReverseProxyTargets(props: {
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
<strong>{t("warning")}:</strong> {t("proxyProtocolWarning")}
|
||||
<strong>
|
||||
{t("warning")}:
|
||||
</strong>{" "}
|
||||
{t("proxyProtocolWarning")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</>
|
||||
@@ -1835,8 +1886,9 @@ export default function ReverseProxyTargets(props: {
|
||||
hcUnhealthyInterval:
|
||||
selectedTargetForHealthCheck.hcUnhealthyInterval ||
|
||||
30,
|
||||
hcTlsServerName: selectedTargetForHealthCheck.hcTlsServerName ||
|
||||
undefined,
|
||||
hcTlsServerName:
|
||||
selectedTargetForHealthCheck.hcTlsServerName ||
|
||||
undefined
|
||||
}}
|
||||
onChanges={async (config) => {
|
||||
if (selectedTargetForHealthCheck) {
|
||||
|
||||
@@ -114,23 +114,25 @@ export default function ResourceRules(props: {
|
||||
const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules);
|
||||
const [openCountrySelect, setOpenCountrySelect] = useState(false);
|
||||
const [countrySelectValue, setCountrySelectValue] = useState("");
|
||||
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
|
||||
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
|
||||
useState(false);
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
|
||||
const isMaxmindAvailable =
|
||||
env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
|
||||
|
||||
const RuleAction = {
|
||||
ACCEPT: t('alwaysAllow'),
|
||||
DROP: t('alwaysDeny'),
|
||||
PASS: t('passToAuth')
|
||||
ACCEPT: t("alwaysAllow"),
|
||||
DROP: t("alwaysDeny"),
|
||||
PASS: t("passToAuth")
|
||||
} as const;
|
||||
|
||||
const RuleMatch = {
|
||||
PATH: t('path'),
|
||||
PATH: t("path"),
|
||||
IP: "IP",
|
||||
CIDR: t('ipAddressRange'),
|
||||
COUNTRY: t('country')
|
||||
CIDR: t("ipAddressRange"),
|
||||
COUNTRY: t("country")
|
||||
} as const;
|
||||
|
||||
const addRuleForm = useForm({
|
||||
@@ -155,10 +157,10 @@ export default function ResourceRules(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorFetch'),
|
||||
title: t("rulesErrorFetch"),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
t('rulesErrorFetchDescription')
|
||||
t("rulesErrorFetchDescription")
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
@@ -179,8 +181,8 @@ export default function ResourceRules(props: {
|
||||
if (isDuplicate) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorDuplicate'),
|
||||
description: t('rulesErrorDuplicateDescription')
|
||||
title: t("rulesErrorDuplicate"),
|
||||
description: t("rulesErrorDuplicateDescription")
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -188,8 +190,8 @@ export default function ResourceRules(props: {
|
||||
if (data.match === "CIDR" && !isValidCIDR(data.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidIpAddressRange'),
|
||||
description: t('rulesErrorInvalidIpAddressRangeDescription')
|
||||
title: t("rulesErrorInvalidIpAddressRange"),
|
||||
description: t("rulesErrorInvalidIpAddressRangeDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -197,8 +199,8 @@ export default function ResourceRules(props: {
|
||||
if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidUrl'),
|
||||
description: t('rulesErrorInvalidUrlDescription')
|
||||
title: t("rulesErrorInvalidUrl"),
|
||||
description: t("rulesErrorInvalidUrlDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -206,17 +208,22 @@ export default function ResourceRules(props: {
|
||||
if (data.match === "IP" && !isValidIP(data.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidIpAddress'),
|
||||
description: t('rulesErrorInvalidIpAddressDescription')
|
||||
title: t("rulesErrorInvalidIpAddress"),
|
||||
description: t("rulesErrorInvalidIpAddressDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
if (data.match === "COUNTRY" && !COUNTRIES.some(c => c.code === data.value)) {
|
||||
if (
|
||||
data.match === "COUNTRY" &&
|
||||
!COUNTRIES.some((c) => c.code === data.value)
|
||||
) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidCountry'),
|
||||
description: t('rulesErrorInvalidCountryDescription') || "Invalid country code."
|
||||
title: t("rulesErrorInvalidCountry"),
|
||||
description:
|
||||
t("rulesErrorInvalidCountryDescription") ||
|
||||
"Invalid country code."
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -265,13 +272,13 @@ export default function ResourceRules(props: {
|
||||
function getValueHelpText(type: string) {
|
||||
switch (type) {
|
||||
case "CIDR":
|
||||
return t('rulesMatchIpAddressRangeDescription');
|
||||
return t("rulesMatchIpAddressRangeDescription");
|
||||
case "IP":
|
||||
return t('rulesMatchIpAddress');
|
||||
return t("rulesMatchIpAddress");
|
||||
case "PATH":
|
||||
return t('rulesMatchUrl');
|
||||
return t("rulesMatchUrl");
|
||||
case "COUNTRY":
|
||||
return t('rulesMatchCountry');
|
||||
return t("rulesMatchCountry");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,10 +295,10 @@ export default function ResourceRules(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorUpdate'),
|
||||
title: t("rulesErrorUpdate"),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
t('rulesErrorUpdateDescription')
|
||||
t("rulesErrorUpdateDescription")
|
||||
)
|
||||
});
|
||||
throw err;
|
||||
@@ -314,8 +321,10 @@ export default function ResourceRules(props: {
|
||||
if (rule.match === "CIDR" && !isValidCIDR(rule.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidIpAddressRange'),
|
||||
description: t('rulesErrorInvalidIpAddressRangeDescription')
|
||||
title: t("rulesErrorInvalidIpAddressRange"),
|
||||
description: t(
|
||||
"rulesErrorInvalidIpAddressRangeDescription"
|
||||
)
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -326,8 +335,8 @@ export default function ResourceRules(props: {
|
||||
) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidUrl'),
|
||||
description: t('rulesErrorInvalidUrlDescription')
|
||||
title: t("rulesErrorInvalidUrl"),
|
||||
description: t("rulesErrorInvalidUrlDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -335,8 +344,8 @@ export default function ResourceRules(props: {
|
||||
if (rule.match === "IP" && !isValidIP(rule.value)) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidIpAddress'),
|
||||
description: t('rulesErrorInvalidIpAddressDescription')
|
||||
title: t("rulesErrorInvalidIpAddress"),
|
||||
description: t("rulesErrorInvalidIpAddressDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -345,8 +354,8 @@ export default function ResourceRules(props: {
|
||||
if (rule.priority === undefined) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidPriority'),
|
||||
description: t('rulesErrorInvalidPriorityDescription')
|
||||
title: t("rulesErrorInvalidPriority"),
|
||||
description: t("rulesErrorInvalidPriorityDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -357,8 +366,8 @@ export default function ResourceRules(props: {
|
||||
if (priorities.length !== new Set(priorities).size) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorDuplicatePriority'),
|
||||
description: t('rulesErrorDuplicatePriorityDescription')
|
||||
title: t("rulesErrorDuplicatePriority"),
|
||||
description: t("rulesErrorDuplicatePriorityDescription")
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -397,8 +406,8 @@ export default function ResourceRules(props: {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t('ruleUpdated'),
|
||||
description: t('ruleUpdatedDescription')
|
||||
title: t("ruleUpdated"),
|
||||
description: t("ruleUpdatedDescription")
|
||||
});
|
||||
|
||||
setRulesToRemove([]);
|
||||
@@ -407,10 +416,10 @@ export default function ResourceRules(props: {
|
||||
console.error(err);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('ruleErrorUpdate'),
|
||||
title: t("ruleErrorUpdate"),
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
t('ruleErrorUpdateDescription')
|
||||
t("ruleErrorUpdateDescription")
|
||||
)
|
||||
});
|
||||
}
|
||||
@@ -428,7 +437,7 @@ export default function ResourceRules(props: {
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t('rulesPriority')}
|
||||
{t("rulesPriority")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
@@ -440,15 +449,19 @@ export default function ResourceRules(props: {
|
||||
type="number"
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onBlur={(e) => {
|
||||
const parsed = z.int()
|
||||
const parsed = z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.safeParse(e.target.value);
|
||||
|
||||
if (!parsed.data) {
|
||||
if (!parsed.success) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t('rulesErrorInvalidIpAddress'), // correct priority or IP?
|
||||
description: t('rulesErrorInvalidPriorityDescription')
|
||||
title: t("rulesErrorInvalidPriority"), // correct priority or IP?
|
||||
description: t(
|
||||
"rulesErrorInvalidPriorityDescription"
|
||||
)
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -463,7 +476,7 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "action",
|
||||
header: () => (<span className="p-3">{t('rulesAction')}</span>),
|
||||
header: () => <span className="p-3">{t("rulesAction")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.action}
|
||||
@@ -486,12 +499,18 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "match",
|
||||
header: () => (<span className="p-3">{t('rulesMatchType')}</span>),
|
||||
header: () => <span className="p-3">{t("rulesMatchType")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.match}
|
||||
onValueChange={(value: "CIDR" | "IP" | "PATH" | "COUNTRY") =>
|
||||
updateRule(row.original.ruleId, { match: value, value: value === "COUNTRY" ? "US" : row.original.value })
|
||||
onValueChange={(
|
||||
value: "CIDR" | "IP" | "PATH" | "COUNTRY"
|
||||
) =>
|
||||
updateRule(row.original.ruleId, {
|
||||
match: value,
|
||||
value:
|
||||
value === "COUNTRY" ? "US" : row.original.value
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="min-w-[125px]">
|
||||
@@ -502,7 +521,9 @@ export default function ResourceRules(props: {
|
||||
<SelectItem value="IP">{RuleMatch.IP}</SelectItem>
|
||||
<SelectItem value="CIDR">{RuleMatch.CIDR}</SelectItem>
|
||||
{isMaxmindAvailable && (
|
||||
<SelectItem value="COUNTRY">{RuleMatch.COUNTRY}</SelectItem>
|
||||
<SelectItem value="COUNTRY">
|
||||
{RuleMatch.COUNTRY}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -510,8 +531,8 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "value",
|
||||
header: () => (<span className="p-3">{t('value')}</span>),
|
||||
cell: ({ row }) => (
|
||||
header: () => <span className="p-3">{t("value")}</span>,
|
||||
cell: ({ row }) =>
|
||||
row.original.match === "COUNTRY" ? (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -521,29 +542,43 @@ export default function ResourceRules(props: {
|
||||
className="min-w-[200px] justify-between"
|
||||
>
|
||||
{row.original.value
|
||||
? COUNTRIES.find((country) => country.code === row.original.value)?.name +
|
||||
" (" + row.original.value + ")"
|
||||
: t('selectCountry')}
|
||||
? COUNTRIES.find(
|
||||
(country) =>
|
||||
country.code ===
|
||||
row.original.value
|
||||
)?.name +
|
||||
" (" +
|
||||
row.original.value +
|
||||
")"
|
||||
: t("selectCountry")}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="min-w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={t('searchCountries')} />
|
||||
<CommandInput
|
||||
placeholder={t("searchCountries")}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t('noCountryFound')}</CommandEmpty>
|
||||
<CommandEmpty>
|
||||
{t("noCountryFound")}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{COUNTRIES.map((country) => (
|
||||
<CommandItem
|
||||
key={country.code}
|
||||
value={country.name}
|
||||
onSelect={() => {
|
||||
updateRule(row.original.ruleId, { value: country.code });
|
||||
updateRule(
|
||||
row.original.ruleId,
|
||||
{ value: country.code }
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
row.original.value === country.code
|
||||
row.original.value ===
|
||||
country.code
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
@@ -567,11 +602,10 @@ export default function ResourceRules(props: {
|
||||
}
|
||||
/>
|
||||
)
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
header: () => (<span className="p-3">{t('enabled')}</span>),
|
||||
header: () => <span className="p-3">{t("enabled")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
defaultChecked={row.original.enabled}
|
||||
@@ -583,14 +617,14 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t('actions')}</span>),
|
||||
header: () => <span className="p-3">{t("actions")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => removeRule(row.original.ruleId)}
|
||||
>
|
||||
{t('delete')}
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@@ -664,10 +698,10 @@ export default function ResourceRules(props: {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t('rulesResource')}
|
||||
{t("rulesResource")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t('rulesResourceDescription')}
|
||||
{t("rulesResourceDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
@@ -675,7 +709,7 @@ export default function ResourceRules(props: {
|
||||
<div className="flex items-center space-x-2">
|
||||
<SwitchInput
|
||||
id="rules-toggle"
|
||||
label={t('rulesEnable')}
|
||||
label={t("rulesEnable")}
|
||||
defaultChecked={rulesEnabled}
|
||||
onCheckedChange={(val) => setRulesEnabled(val)}
|
||||
/>
|
||||
@@ -692,7 +726,9 @@ export default function ResourceRules(props: {
|
||||
name="action"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('rulesAction')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("rulesAction")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
@@ -705,13 +741,19 @@ export default function ResourceRules(props: {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ACCEPT">
|
||||
{RuleAction.ACCEPT}
|
||||
{
|
||||
RuleAction.ACCEPT
|
||||
}
|
||||
</SelectItem>
|
||||
<SelectItem value="DROP">
|
||||
{RuleAction.DROP}
|
||||
{
|
||||
RuleAction.DROP
|
||||
}
|
||||
</SelectItem>
|
||||
<SelectItem value="PASS">
|
||||
{RuleAction.PASS}
|
||||
{
|
||||
RuleAction.PASS
|
||||
}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -725,11 +767,15 @@ export default function ResourceRules(props: {
|
||||
name="match"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('rulesMatchType')}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("rulesMatchType")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
@@ -737,7 +783,9 @@ export default function ResourceRules(props: {
|
||||
<SelectContent>
|
||||
{resource.http && (
|
||||
<SelectItem value="PATH">
|
||||
{RuleMatch.PATH}
|
||||
{
|
||||
RuleMatch.PATH
|
||||
}
|
||||
</SelectItem>
|
||||
)}
|
||||
<SelectItem value="IP">
|
||||
@@ -748,7 +796,9 @@ export default function ResourceRules(props: {
|
||||
</SelectItem>
|
||||
{isMaxmindAvailable && (
|
||||
<SelectItem value="COUNTRY">
|
||||
{RuleMatch.COUNTRY}
|
||||
{
|
||||
RuleMatch.COUNTRY
|
||||
}
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
@@ -764,7 +814,7 @@ export default function ResourceRules(props: {
|
||||
render={({ field }) => (
|
||||
<FormItem className="gap-1">
|
||||
<InfoPopup
|
||||
text={t('value')}
|
||||
text={t("value")}
|
||||
info={
|
||||
getValueHelpText(
|
||||
addRuleForm.watch(
|
||||
@@ -774,47 +824,100 @@ export default function ResourceRules(props: {
|
||||
}
|
||||
/>
|
||||
<FormControl>
|
||||
{addRuleForm.watch("match") === "COUNTRY" ? (
|
||||
<Popover open={openAddRuleCountrySelect} onOpenChange={setOpenAddRuleCountrySelect}>
|
||||
<PopoverTrigger asChild>
|
||||
{addRuleForm.watch(
|
||||
"match"
|
||||
) === "COUNTRY" ? (
|
||||
<Popover
|
||||
open={
|
||||
openAddRuleCountrySelect
|
||||
}
|
||||
onOpenChange={
|
||||
setOpenAddRuleCountrySelect
|
||||
}
|
||||
>
|
||||
<PopoverTrigger
|
||||
asChild
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openAddRuleCountrySelect}
|
||||
aria-expanded={
|
||||
openAddRuleCountrySelect
|
||||
}
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{field.value
|
||||
? COUNTRIES.find((country) => country.code === field.value)?.name +
|
||||
" (" + field.value + ")"
|
||||
: t('selectCountry')}
|
||||
? COUNTRIES.find(
|
||||
(
|
||||
country
|
||||
) =>
|
||||
country.code ===
|
||||
field.value
|
||||
)
|
||||
?.name +
|
||||
" (" +
|
||||
field.value +
|
||||
")"
|
||||
: t(
|
||||
"selectCountry"
|
||||
)}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={t('searchCountries')} />
|
||||
<CommandInput
|
||||
placeholder={t(
|
||||
"searchCountries"
|
||||
)}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t('noCountryFound')}</CommandEmpty>
|
||||
<CommandEmpty>
|
||||
{t(
|
||||
"noCountryFound"
|
||||
)}
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{COUNTRIES.map((country) => (
|
||||
<CommandItem
|
||||
key={country.code}
|
||||
value={country.name}
|
||||
onSelect={() => {
|
||||
field.onChange(country.code);
|
||||
setOpenAddRuleCountrySelect(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
field.value === country.code
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
{country.name} ({country.code})
|
||||
</CommandItem>
|
||||
))}
|
||||
{COUNTRIES.map(
|
||||
(
|
||||
country
|
||||
) => (
|
||||
<CommandItem
|
||||
key={
|
||||
country.code
|
||||
}
|
||||
value={
|
||||
country.name
|
||||
}
|
||||
onSelect={() => {
|
||||
field.onChange(
|
||||
country.code
|
||||
);
|
||||
setOpenAddRuleCountrySelect(
|
||||
false
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={`mr-2 h-4 w-4 ${
|
||||
field.value ===
|
||||
country.code
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
}`}
|
||||
/>
|
||||
{
|
||||
country.name
|
||||
}{" "}
|
||||
(
|
||||
{
|
||||
country.code
|
||||
}
|
||||
|
||||
)
|
||||
</CommandItem>
|
||||
)
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
@@ -833,7 +936,7 @@ export default function ResourceRules(props: {
|
||||
variant="outline"
|
||||
disabled={!rulesEnabled}
|
||||
>
|
||||
{t('ruleSubmit')}
|
||||
{t("ruleSubmit")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -843,16 +946,22 @@ export default function ResourceRules(props: {
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
const isActionsColumn = header.column.id === "actions";
|
||||
const isActionsColumn =
|
||||
header.column.id === "actions";
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
|
||||
className={
|
||||
isActionsColumn
|
||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef
|
||||
header.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
@@ -866,20 +975,30 @@ export default function ResourceRules(props: {
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
const isActionsColumn = cell.column.id === "actions";
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
{row
|
||||
.getVisibleCells()
|
||||
.map((cell) => {
|
||||
const isActionsColumn =
|
||||
cell.column.id ===
|
||||
"actions";
|
||||
return (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className={
|
||||
isActionsColumn
|
||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
@@ -888,7 +1007,7 @@ export default function ResourceRules(props: {
|
||||
colSpan={columns.length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{t('rulesNoOne')}
|
||||
{t("rulesNoOne")}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
@@ -907,7 +1026,7 @@ export default function ResourceRules(props: {
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('saveAllSettings')}
|
||||
{t("saveAllSettings")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsContainer>
|
||||
|
||||
@@ -190,8 +190,12 @@ const addTargetSchema = z
|
||||
return false;
|
||||
}
|
||||
// If rewritePathType is provided, rewritePath must be provided
|
||||
// Exception: stripPrefix can have an empty rewritePath (to just strip the prefix)
|
||||
if (data.rewritePathType && !data.rewritePath) {
|
||||
return false;
|
||||
// Allow empty rewritePath for stripPrefix type
|
||||
if (data.rewritePathType !== "stripPrefix") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@@ -432,16 +436,16 @@ export default function Page() {
|
||||
|
||||
const newTarget: LocalTarget = {
|
||||
...data,
|
||||
path: isHttp ? (data.path || null) : null,
|
||||
pathMatchType: isHttp ? (data.pathMatchType || null) : null,
|
||||
rewritePath: isHttp ? (data.rewritePath || null) : null,
|
||||
rewritePathType: isHttp ? (data.rewritePathType || null) : null,
|
||||
path: isHttp ? data.path || null : null,
|
||||
pathMatchType: isHttp ? data.pathMatchType || null : null,
|
||||
rewritePath: isHttp ? data.rewritePath || null : null,
|
||||
rewritePathType: isHttp ? data.rewritePathType || null : null,
|
||||
siteType: site?.type || null,
|
||||
enabled: true,
|
||||
targetId: new Date().getTime(),
|
||||
new: true,
|
||||
resourceId: 0, // Will be set when resource is created
|
||||
priority: isHttp ? (data.priority || 100) : 100, // Default priority
|
||||
priority: isHttp ? data.priority || 100 : 100, // Default priority
|
||||
hcEnabled: false,
|
||||
hcPath: null,
|
||||
hcMethod: null,
|
||||
@@ -507,7 +511,7 @@ export default function Page() {
|
||||
try {
|
||||
const payload = {
|
||||
name: baseData.name,
|
||||
http: baseData.http,
|
||||
http: baseData.http
|
||||
};
|
||||
|
||||
let sanitizedSubdomain: string | undefined;
|
||||
@@ -577,7 +581,8 @@ export default function Page() {
|
||||
hcFollowRedirects:
|
||||
target.hcFollowRedirects || null,
|
||||
hcStatus: target.hcStatus || null,
|
||||
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
||||
hcUnhealthyInterval:
|
||||
target.hcUnhealthyInterval || null,
|
||||
hcMode: target.hcMode || null,
|
||||
hcTlsServerName: target.hcTlsServerName
|
||||
};
|
||||
@@ -737,7 +742,7 @@ export default function Page() {
|
||||
const priorityColumn: ColumnDef<LocalTarget> = {
|
||||
id: "priority",
|
||||
header: () => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
{t("priority")}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
@@ -780,7 +785,7 @@ export default function Page() {
|
||||
|
||||
const healthCheckColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "healthCheck",
|
||||
header: () => (<span className="p-3">{t("healthCheck")}</span>),
|
||||
header: () => <span className="p-3">{t("healthCheck")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.hcHealth || "unknown";
|
||||
const isEnabled = row.original.hcEnabled;
|
||||
@@ -826,18 +831,16 @@ export default function Page() {
|
||||
{row.original.siteType === "newt" ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex items-center justify-between gap-2 p-2 w-full text-left cursor-pointer"
|
||||
className="flex items-center gap-2 w-full text-left cursor-pointer"
|
||||
onClick={() =>
|
||||
openHealthCheckDialog(row.original)
|
||||
}
|
||||
>
|
||||
<Badge variant={getStatusColor(status)}>
|
||||
<div className="flex items-center gap-1">
|
||||
{getStatusIcon(status)}
|
||||
{getStatusText(status)}
|
||||
</div>
|
||||
</Badge>
|
||||
<Settings className="h-4 w-4" />
|
||||
<div className="flex items-center gap-1">
|
||||
{getStatusIcon(status)}
|
||||
{getStatusText(status)}
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
<span>-</span>
|
||||
@@ -852,7 +855,7 @@ export default function Page() {
|
||||
|
||||
const matchPathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "path",
|
||||
header: () => (<span className="p-3">{t("matchPath")}</span>),
|
||||
header: () => <span className="p-3">{t("matchPath")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const hasPathMatch = !!(
|
||||
row.original.path || row.original.pathMatchType
|
||||
@@ -914,7 +917,7 @@ export default function Page() {
|
||||
|
||||
const addressColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "address",
|
||||
header: () => (<span className="p-3">{t("address")}</span>),
|
||||
header: () => <span className="p-3">{t("address")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const selectedSite = sites.find(
|
||||
(site) => site.siteId === row.original.siteId
|
||||
@@ -1035,8 +1038,12 @@ export default function Page() {
|
||||
{row.original.method || "http"}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="http">http</SelectItem>
|
||||
<SelectItem value="https">https</SelectItem>
|
||||
<SelectItem value="http">
|
||||
http
|
||||
</SelectItem>
|
||||
<SelectItem value="https">
|
||||
https
|
||||
</SelectItem>
|
||||
<SelectItem value="h2c">h2c</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
@@ -1050,7 +1057,7 @@ export default function Page() {
|
||||
|
||||
<Input
|
||||
defaultValue={row.original.ip}
|
||||
placeholder="IP / Hostname"
|
||||
placeholder="Host"
|
||||
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
|
||||
onBlur={(e) => {
|
||||
const input = e.target.value.trim();
|
||||
@@ -1128,7 +1135,7 @@ export default function Page() {
|
||||
|
||||
const rewritePathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "rewritePath",
|
||||
header: () => (<span className="p-3">{t("rewritePath")}</span>),
|
||||
header: () => <span className="p-3">{t("rewritePath")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const hasRewritePath = !!(
|
||||
row.original.rewritePath || row.original.rewritePathType
|
||||
@@ -1198,7 +1205,7 @@ export default function Page() {
|
||||
|
||||
const enabledColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "enabled",
|
||||
header: () => (<span className="p-3">{t("enabled")}</span>),
|
||||
header: () => <span className="p-3">{t("enabled")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<Switch
|
||||
@@ -1219,7 +1226,7 @@ export default function Page() {
|
||||
|
||||
const actionsColumn: ColumnDef<LocalTarget> = {
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
header: () => <span className="p-3">{t("actions")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end w-full">
|
||||
<Button
|
||||
@@ -1341,42 +1348,38 @@ export default function Page() {
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
|
||||
{resourceTypes.length > 1 && (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<span className="text-sm font-medium">
|
||||
{t("type")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<StrategySelect
|
||||
options={resourceTypes}
|
||||
defaultValue="http"
|
||||
onChange={(value) => {
|
||||
baseForm.setValue(
|
||||
"http",
|
||||
value === "http"
|
||||
);
|
||||
// Update method default when switching resource type
|
||||
addTargetForm.setValue(
|
||||
"method",
|
||||
value === "http"
|
||||
? "http"
|
||||
: null
|
||||
);
|
||||
}}
|
||||
cols={2}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
{resourceTypes.length > 1 && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourceType")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourceTypeDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<StrategySelect
|
||||
options={resourceTypes}
|
||||
defaultValue="http"
|
||||
onChange={(value) => {
|
||||
baseForm.setValue(
|
||||
"http",
|
||||
value === "http"
|
||||
);
|
||||
// Update method default when switching resource type
|
||||
addTargetForm.setValue(
|
||||
"method",
|
||||
value === "http"
|
||||
? "http"
|
||||
: null
|
||||
);
|
||||
}}
|
||||
cols={2}
|
||||
/>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{baseForm.watch("http") ? (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
@@ -1393,6 +1396,8 @@ export default function Page() {
|
||||
<DomainPicker
|
||||
orgId={orgId as string}
|
||||
onDomainChange={(res) => {
|
||||
if (!res) return;
|
||||
|
||||
httpForm.setValue(
|
||||
"subdomain",
|
||||
res.subdomain
|
||||
@@ -1422,146 +1427,98 @@ export default function Page() {
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...tcpUdpForm}>
|
||||
<form
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault(); // block default enter refresh
|
||||
}
|
||||
}}
|
||||
className="space-y-4"
|
||||
id="tcp-udp-settings-form"
|
||||
>
|
||||
<Controller
|
||||
control={
|
||||
tcpUdpForm.control
|
||||
}
|
||||
name="protocol"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"protocol"
|
||||
)}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
{...field}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"protocolSelect"
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">
|
||||
TCP
|
||||
</SelectItem>
|
||||
<SelectItem value="udp">
|
||||
UDP
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={
|
||||
tcpUdpForm.control
|
||||
}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"resourcePortNumber"
|
||||
)}
|
||||
</FormLabel>
|
||||
<Form {...tcpUdpForm}>
|
||||
<form
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault(); // block default enter refresh
|
||||
}
|
||||
}}
|
||||
className="space-y-4 grid gap-4 grid-cols-1 md:grid-cols-2 items-start"
|
||||
id="tcp-udp-settings-form"
|
||||
>
|
||||
<Controller
|
||||
control={tcpUdpForm.control}
|
||||
name="protocol"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("protocol")}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
{...field}
|
||||
>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
field.onChange(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourcePortNumberDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* {build == "oss" && (
|
||||
<FormField
|
||||
control={
|
||||
tcpUdpForm.control
|
||||
}
|
||||
name="enableProxy"
|
||||
render={({
|
||||
field
|
||||
}) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
variant={
|
||||
"outlinePrimarySquare"
|
||||
}
|
||||
checked={
|
||||
field.value
|
||||
}
|
||||
onCheckedChange={
|
||||
field.onChange
|
||||
}
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"protocolSelect"
|
||||
)}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
{t(
|
||||
"resourceEnableProxy"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourceEnableProxyDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)} */}
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">
|
||||
TCP
|
||||
</SelectItem>
|
||||
<SelectItem value="udp">
|
||||
UDP
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={tcpUdpForm.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"resourcePortNumber"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
field.onChange(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
? parseInt(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourcePortNumberDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
)}
|
||||
@@ -1596,13 +1553,21 @@ export default function Page() {
|
||||
(
|
||||
header
|
||||
) => {
|
||||
const isActionsColumn = header.column.id === "actions";
|
||||
const isActionsColumn =
|
||||
header
|
||||
.column
|
||||
.id ===
|
||||
"actions";
|
||||
return (
|
||||
<TableHead
|
||||
key={
|
||||
header.id
|
||||
}
|
||||
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
|
||||
className={
|
||||
isActionsColumn
|
||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
@@ -1639,13 +1604,21 @@ export default function Page() {
|
||||
(
|
||||
cell
|
||||
) => {
|
||||
const isActionsColumn = cell.column.id === "actions";
|
||||
const isActionsColumn =
|
||||
cell
|
||||
.column
|
||||
.id ===
|
||||
"actions";
|
||||
return (
|
||||
<TableCell
|
||||
key={
|
||||
cell.id
|
||||
}
|
||||
className={isActionsColumn ? "sticky right-0 z-10 w-auto min-w-fit bg-card" : ""}
|
||||
className={
|
||||
isActionsColumn
|
||||
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{flexRender(
|
||||
cell
|
||||
@@ -1711,7 +1684,7 @@ export default function Page() {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-8 border-2 border-dashed border-muted rounded-lg p-4">
|
||||
<div className="text-center p-4">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{t("targetNoOne")}
|
||||
</p>
|
||||
@@ -1877,7 +1850,7 @@ export default function Page() {
|
||||
|
||||
<Link
|
||||
className="text-sm text-primary flex items-center gap-1"
|
||||
href="https://docs.pangolin.net/manage/resources/tcp-udp-resources"
|
||||
href="https://docs.pangolin.net/manage/resources/public/raw-resources"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -7,7 +7,9 @@ import { cache } from "react";
|
||||
import { GetOrgResponse } from "@server/routers/org";
|
||||
import OrgProvider from "@app/providers/OrgProvider";
|
||||
import { ListAccessTokensResponse } from "@server/routers/accessToken";
|
||||
import ShareLinksTable, { ShareLinkRow } from "../../../../components/ShareLinksTable";
|
||||
import ShareLinksTable, {
|
||||
ShareLinkRow
|
||||
} from "../../../../components/ShareLinksTable";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type ShareLinksPageProps = {
|
||||
@@ -58,8 +60,8 @@ export default async function ShareLinksPage(props: ShareLinksPageProps) {
|
||||
{/* <ShareableLinksSplash /> */}
|
||||
|
||||
<SettingsSectionTitle
|
||||
title={t('shareTitle')}
|
||||
description={t('shareDescription')}
|
||||
title={t("shareTitle")}
|
||||
description={t("shareDescription")}
|
||||
/>
|
||||
|
||||
<OrgProvider org={org}>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionFooter,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
@@ -18,11 +19,26 @@ import { useTranslations } from "next-intl";
|
||||
import { PickSiteDefaultsResponse } from "@server/routers/site";
|
||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||
import { generateKeypair } from "../wireguardConfig";
|
||||
import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip";
|
||||
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() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -33,9 +49,20 @@ export default function CredentialsPage() {
|
||||
const { site } = useSiteContext();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [siteDefaults, setSiteDefaults] = useState<PickSiteDefaultsResponse | null>(null);
|
||||
const [siteDefaults, setSiteDefaults] =
|
||||
useState<PickSiteDefaultsResponse | null>(null);
|
||||
const [wgConfig, setWgConfig] = useState("");
|
||||
const [publicKey, setPublicKey] = useState("");
|
||||
const [currentNewtId, setCurrentNewtId] = useState<string | null>(
|
||||
site.newtId
|
||||
);
|
||||
const [regeneratedSecret, setRegeneratedSecret] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [showCredentialsAlert, setShowCredentialsAlert] = useState(false);
|
||||
const [showWireGuardAlert, setShowWireGuardAlert] = useState(false);
|
||||
const [loadingDefaults, setLoadingDefaults] = useState(false);
|
||||
const [shouldDisconnect, setShouldDisconnect] = useState(true);
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
@@ -47,147 +74,394 @@ export default function CredentialsPage() {
|
||||
return isEnterpriseNotLicensed || isSaasNotSubscribed;
|
||||
};
|
||||
|
||||
|
||||
const hydrateWireGuardConfig = (
|
||||
privateKey: string,
|
||||
publicKey: string,
|
||||
subnet: string,
|
||||
address: string,
|
||||
endpoint: string,
|
||||
listenPort: string
|
||||
) => {
|
||||
const config = `[Interface]
|
||||
Address = ${subnet}
|
||||
ListenPort = 51820
|
||||
PrivateKey = ${privateKey}
|
||||
|
||||
[Peer]
|
||||
PublicKey = ${publicKey}
|
||||
AllowedIPs = ${address.split("/")[0]}/32
|
||||
Endpoint = ${endpoint}:${listenPort}
|
||||
PersistentKeepalive = 5`;
|
||||
setWgConfig(config);
|
||||
return config;
|
||||
};
|
||||
// Fetch site defaults for wireguard sites to show in obfuscated config
|
||||
useEffect(() => {
|
||||
const fetchSiteDefaults = async () => {
|
||||
if (site?.type === "wireguard" && !siteDefaults && orgId) {
|
||||
setLoadingDefaults(true);
|
||||
try {
|
||||
const res = await api.get(
|
||||
`/org/${orgId}/pick-site-defaults`
|
||||
);
|
||||
if (res && res.status === 200) {
|
||||
setSiteDefaults(res.data.data);
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail - we'll use site data or obfuscated values
|
||||
} finally {
|
||||
setLoadingDefaults(false);
|
||||
}
|
||||
} else {
|
||||
setLoadingDefaults(false);
|
||||
}
|
||||
};
|
||||
fetchSiteDefaults();
|
||||
}, []);
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
let generatedPublicKey = "";
|
||||
let generatedWgConfig = "";
|
||||
try {
|
||||
let generatedPublicKey = "";
|
||||
let generatedWgConfig = "";
|
||||
|
||||
if (site?.type === "wireguard") {
|
||||
const generatedKeypair = generateKeypair();
|
||||
generatedPublicKey = generatedKeypair.publicKey;
|
||||
setPublicKey(generatedPublicKey);
|
||||
if (site?.type === "wireguard") {
|
||||
const generatedKeypair = generateKeypair();
|
||||
generatedPublicKey = generatedKeypair.publicKey;
|
||||
setPublicKey(generatedPublicKey);
|
||||
|
||||
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data.data;
|
||||
setSiteDefaults(data);
|
||||
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data.data;
|
||||
setSiteDefaults(data);
|
||||
|
||||
// generate config with the fetched data
|
||||
generatedWgConfig = hydrateWireGuardConfig(
|
||||
generatedKeypair.privateKey,
|
||||
data.publicKey,
|
||||
data.subnet,
|
||||
data.address,
|
||||
data.endpoint,
|
||||
data.listenPort
|
||||
// generate config with the fetched data
|
||||
generatedWgConfig = generateWireGuardConfig(
|
||||
generatedKeypair.privateKey,
|
||||
data.publicKey,
|
||||
data.subnet,
|
||||
data.address,
|
||||
data.endpoint,
|
||||
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`, {
|
||||
type: "wireguard",
|
||||
subnet: res.data.data.subnet,
|
||||
exitNodeId: res.data.data.exitNodeId,
|
||||
pubKey: generatedPublicKey
|
||||
if (site?.type === "newt") {
|
||||
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data.data;
|
||||
|
||||
const rekeyRes = await api.post(
|
||||
`/re-key/${site?.siteId}/regenerate-site-secret`,
|
||||
{
|
||||
type: "newt",
|
||||
secret: data.newtSecret,
|
||||
disconnect: shouldDisconnect
|
||||
}
|
||||
);
|
||||
|
||||
if (rekeyRes && rekeyRes.status === 200) {
|
||||
const rekeyData = rekeyRes.data.data;
|
||||
if (rekeyData && rekeyData.newtId) {
|
||||
setCurrentNewtId(rekeyData.newtId);
|
||||
setRegeneratedSecret(data.newtSecret);
|
||||
setSiteDefaults({
|
||||
...data,
|
||||
newtId: rekeyData.newtId
|
||||
});
|
||||
setShowCredentialsAlert(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t("credentialsSaved"),
|
||||
description: t("credentialsSavedDescription")
|
||||
});
|
||||
|
||||
// ConfirmDeleteDialog handles closing the modal and triggering refresh via setOpen callback
|
||||
} catch (error) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("error") || "Error",
|
||||
description:
|
||||
formatAxiosError(error) ||
|
||||
t("credentialsRegenerateError") ||
|
||||
"Failed to regenerate credentials"
|
||||
});
|
||||
}
|
||||
|
||||
if (site?.type === "newt") {
|
||||
const res = await api.get(`/org/${orgId}/pick-site-defaults`);
|
||||
if (res && res.status === 200) {
|
||||
const data = res.data.data;
|
||||
setSiteDefaults(data);
|
||||
|
||||
await api.post(`/re-key/${site?.siteId}/regenerate-site-secret`, {
|
||||
type: "newt",
|
||||
newtId: data.newtId,
|
||||
newtSecret: data.newtSecret
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t("credentialsSaved"),
|
||||
description: t("credentialsSavedDescription")
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const getCredentialType = () => {
|
||||
if (site?.type === "wireguard") return "site-wireguard";
|
||||
if (site?.type === "newt") return "site-newt";
|
||||
return "site-newt";
|
||||
const getConfirmationString = () => {
|
||||
return site?.name || site?.niceId || "My site";
|
||||
};
|
||||
|
||||
const getCredentials = () => {
|
||||
if (site?.type === "wireguard" && wgConfig) {
|
||||
return { wgConfig };
|
||||
}
|
||||
if (site?.type === "newt" && siteDefaults) {
|
||||
return {
|
||||
Id: siteDefaults.newtId,
|
||||
Secret: siteDefaults.newtSecret
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const displayNewtId = currentNewtId || siteDefaults?.newtId || null;
|
||||
const displaySecret = regeneratedSecret || null;
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("generatedcredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("regenerateCredentials")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<>
|
||||
<SettingsContainer>
|
||||
{site?.type === "newt" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("siteNewtCredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("siteNewtCredentialsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-block">
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("regeneratecredentials")}
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<SecurityFeaturesAlert />
|
||||
|
||||
{isSecurityFeatureDisabled() && (
|
||||
<TooltipContent side="top">
|
||||
{t("featureDisabledTooltip")}
|
||||
</TooltipContent>
|
||||
<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>
|
||||
|
||||
{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>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
</SettingsSectionBody>
|
||||
{build !== "oss" && (
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShouldDisconnect(false);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("regenerateCredentialsButton")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShouldDisconnect(true);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("siteRegenerateAndDisconnect")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
)}
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
<RegenerateCredentialsModal
|
||||
open={modalOpen}
|
||||
onOpenChange={setModalOpen}
|
||||
type={getCredentialType()}
|
||||
onConfirmRegenerate={handleConfirmRegenerate}
|
||||
dashboardUrl={env.app.dashboardUrl}
|
||||
credentials={getCredentials()}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
{site?.type === "wireguard" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("generatedcredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("regenerateCredentials")}
|
||||
</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>
|
||||
{build === "enterprise" && (
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
onClick={() => setModalOpen(true)}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("siteRegenerateAndDisconnect")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
)}
|
||||
</SettingsSection>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
|
||||
{site?.type === "newt" && (
|
||||
<ConfirmDeleteDialog
|
||||
open={modalOpen}
|
||||
setOpen={(val) => {
|
||||
setModalOpen(val);
|
||||
// Prevent modal from reopening during refresh
|
||||
if (!val) {
|
||||
setTimeout(() => {
|
||||
router.refresh();
|
||||
}, 150);
|
||||
}
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-2">
|
||||
{shouldDisconnect ? (
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
"siteRegenerateAndDisconnectConfirmation"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
"siteRegenerateAndDisconnectWarning"
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
"siteRegenerateCredentialsConfirmation"
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t("siteRegenerateCredentialsWarning")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
buttonText={
|
||||
shouldDisconnect
|
||||
? t("siteRegenerateAndDisconnect")
|
||||
: t("regenerateCredentialsButton")
|
||||
}
|
||||
onConfirm={handleConfirmRegenerate}
|
||||
string={getConfirmationString()}
|
||||
title={t("regenerateCredentials")}
|
||||
warningText={t("cannotbeUndone")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{site?.type === "wireguard" && (
|
||||
<ConfirmDeleteDialog
|
||||
open={modalOpen}
|
||||
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")}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import Link from "next/link";
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().nonempty("Name is required"),
|
||||
niceId: z.string().min(1).max(255).optional(),
|
||||
dockerSocketEnabled: z.boolean().optional(),
|
||||
dockerSocketEnabled: z.boolean().optional()
|
||||
});
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
@@ -52,14 +52,16 @@ export default function GeneralPage() {
|
||||
const { toast } = useToast();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(null);
|
||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
defaultValues: {
|
||||
name: site?.name,
|
||||
niceId: site?.niceId || "",
|
||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false,
|
||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
@@ -71,17 +73,19 @@ export default function GeneralPage() {
|
||||
await api.post(`/site/${site?.siteId}`, {
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled
|
||||
});
|
||||
|
||||
updateSite({
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled
|
||||
});
|
||||
|
||||
if (data.niceId && data.niceId !== site?.niceId) {
|
||||
router.replace(`/${site?.orgId}/settings/sites/${data.niceId}/general`);
|
||||
router.replace(
|
||||
`/${site?.orgId}/settings/sites/${data.niceId}/general`
|
||||
);
|
||||
}
|
||||
|
||||
toast({
|
||||
@@ -92,7 +96,10 @@ export default function GeneralPage() {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("siteErrorUpdate"),
|
||||
description: formatAxiosError(e, t("siteErrorUpdateDescription"))
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("siteErrorUpdateDescription")
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,11 +147,15 @@ export default function GeneralPage() {
|
||||
name="niceId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("identifier")}</FormLabel>
|
||||
<FormLabel>
|
||||
{t("identifier")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder={t("enterIdentifier")}
|
||||
placeholder={t(
|
||||
"enterIdentifier"
|
||||
)}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -35,25 +35,24 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('general'),
|
||||
href: `/${params.orgId}/settings/sites/${params.niceId}/general`,
|
||||
title: t("general"),
|
||||
href: `/${params.orgId}/settings/sites/${params.niceId}/general`
|
||||
},
|
||||
...(site.type !== 'local' && build === 'enterprise'
|
||||
...(site.type !== "local"
|
||||
? [
|
||||
{
|
||||
title: t('credentials'),
|
||||
href: `/${params.orgId}/settings/sites/${params.niceId}/credentials`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("credentials"),
|
||||
href: `/${params.orgId}/settings/sites/${params.niceId}/credentials`
|
||||
}
|
||||
]
|
||||
: [])
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('siteSetting', { siteName: site?.name })}
|
||||
description={t('siteSettingDescription')}
|
||||
title={t("siteSetting", { siteName: site?.name })}
|
||||
description={t("siteSettingDescription")}
|
||||
/>
|
||||
|
||||
<SiteProvider site={site}>
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
function gf(init: number[] | undefined = undefined) {
|
||||
var r = new Float64Array(16);
|
||||
if (init) {
|
||||
for (var i = 0; i < init.length; ++i)
|
||||
r[i] = init[i];
|
||||
for (var i = 0; i < init.length; ++i) r[i] = init[i];
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
function pack(o: Uint8Array, n: Float64Array) {
|
||||
var b, m = gf(), t = gf();
|
||||
for (var i = 0; i < 16; ++i)
|
||||
t[i] = n[i];
|
||||
var b,
|
||||
m = gf(),
|
||||
t = gf();
|
||||
for (var i = 0; i < 16; ++i) t[i] = n[i];
|
||||
carry(t);
|
||||
carry(t);
|
||||
carry(t);
|
||||
@@ -45,7 +45,8 @@ function carry(o: Float64Array) {
|
||||
}
|
||||
|
||||
function cswap(p: Float64Array, q: Float64Array, b: number) {
|
||||
var t, c = ~(b - 1);
|
||||
var t,
|
||||
c = ~(b - 1);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
t = c & (p[i] ^ q[i]);
|
||||
p[i] ^= t;
|
||||
@@ -54,40 +55,32 @@ function cswap(p: Float64Array, q: Float64Array, b: number) {
|
||||
}
|
||||
|
||||
function add(o: Float64Array, a: Float64Array, b: Float64Array) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] + b[i]) | 0;
|
||||
for (var i = 0; i < 16; ++i) o[i] = (a[i] + b[i]) | 0;
|
||||
}
|
||||
|
||||
function subtract(o: Float64Array, a: Float64Array, b: Float64Array) {
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = (a[i] - b[i]) | 0;
|
||||
for (var i = 0; i < 16; ++i) o[i] = (a[i] - b[i]) | 0;
|
||||
}
|
||||
|
||||
function multmod(o: Float64Array, a: Float64Array, b: Float64Array) {
|
||||
var t = new Float64Array(31);
|
||||
for (var i = 0; i < 16; ++i) {
|
||||
for (var j = 0; j < 16; ++j)
|
||||
t[i + j] += a[i] * b[j];
|
||||
for (var j = 0; j < 16; ++j) t[i + j] += a[i] * b[j];
|
||||
}
|
||||
for (var i = 0; i < 15; ++i)
|
||||
t[i] += 38 * t[i + 16];
|
||||
for (var i = 0; i < 16; ++i)
|
||||
o[i] = t[i];
|
||||
for (var i = 0; i < 15; ++i) t[i] += 38 * t[i + 16];
|
||||
for (var i = 0; i < 16; ++i) o[i] = t[i];
|
||||
carry(o);
|
||||
carry(o);
|
||||
}
|
||||
|
||||
function invert(o: Float64Array, i: Float64Array) {
|
||||
var c = gf();
|
||||
for (var a = 0; a < 16; ++a)
|
||||
c[a] = i[a];
|
||||
for (var a = 0; a < 16; ++a) c[a] = i[a];
|
||||
for (var a = 253; a >= 0; --a) {
|
||||
multmod(c, c, c);
|
||||
if (a !== 2 && a !== 4)
|
||||
multmod(c, c, i);
|
||||
if (a !== 2 && a !== 4) multmod(c, c, i);
|
||||
}
|
||||
for (var a = 0; a < 16; ++a)
|
||||
o[a] = c[a];
|
||||
for (var a = 0; a < 16; ++a) o[a] = c[a];
|
||||
}
|
||||
|
||||
function clamp(z: Uint8Array) {
|
||||
@@ -96,7 +89,8 @@ function clamp(z: Uint8Array) {
|
||||
}
|
||||
|
||||
function generatePublicKey(privateKey: Uint8Array) {
|
||||
var r, z = new Uint8Array(32);
|
||||
var r,
|
||||
z = new Uint8Array(32);
|
||||
var a = gf([1]),
|
||||
b = gf([9]),
|
||||
c = gf(),
|
||||
@@ -105,8 +99,7 @@ function generatePublicKey(privateKey: Uint8Array) {
|
||||
f = gf(),
|
||||
_121665 = gf([0xdb41, 1]),
|
||||
_9 = gf([9]);
|
||||
for (var i = 0; i < 32; ++i)
|
||||
z[i] = privateKey[i];
|
||||
for (var i = 0; i < 32; ++i) z[i] = privateKey[i];
|
||||
clamp(z);
|
||||
for (var i = 254; i >= 0; --i) {
|
||||
r = (z[i >>> 3] >>> (i & 7)) & 1;
|
||||
@@ -152,9 +145,16 @@ function generatePrivateKey() {
|
||||
}
|
||||
|
||||
function encodeBase64(dest: Uint8Array, src: Uint8Array) {
|
||||
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63]);
|
||||
var input = Uint8Array.from([
|
||||
(src[0] >> 2) & 63,
|
||||
((src[0] << 4) | (src[1] >> 4)) & 63,
|
||||
((src[1] << 2) | (src[2] >> 6)) & 63,
|
||||
src[2] & 63
|
||||
]);
|
||||
for (var i = 0; i < 4; ++i)
|
||||
dest[i] = input[i] + 65 +
|
||||
dest[i] =
|
||||
input[i] +
|
||||
65 +
|
||||
(((25 - input[i]) >> 8) & 6) -
|
||||
(((51 - input[i]) >> 8) & 75) -
|
||||
(((61 - input[i]) >> 8) & 15) +
|
||||
@@ -162,10 +162,14 @@ function encodeBase64(dest: Uint8Array, src: Uint8Array) {
|
||||
}
|
||||
|
||||
function keyToBase64(key: Uint8Array) {
|
||||
var i, base64 = new Uint8Array(44);
|
||||
var i,
|
||||
base64 = new Uint8Array(44);
|
||||
for (i = 0; i < 32 / 3; ++i)
|
||||
encodeBase64(base64.subarray(i * 4), key.subarray(i * 3));
|
||||
encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]));
|
||||
encodeBase64(
|
||||
base64.subarray(i * 4),
|
||||
Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0])
|
||||
);
|
||||
base64[43] = 61;
|
||||
return String.fromCharCode.apply(null, base64 as any);
|
||||
}
|
||||
@@ -177,4 +181,4 @@ export function generateKeypair() {
|
||||
publicKey: keyToBase64(publicKey),
|
||||
privateKey: keyToBase64(privateKey)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { generateKeypair } from "../[niceId]/wireguardConfig";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { generateWireGuardConfig } from "@app/lib/wireguard";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import {
|
||||
CreateSiteBody,
|
||||
@@ -214,27 +215,6 @@ export default function Page() {
|
||||
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 = (
|
||||
id: string,
|
||||
secret: string,
|
||||
@@ -252,7 +232,7 @@ PersistentKeepalive = 5`;
|
||||
All: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -fsSL https://pangolin.net/get-newt.sh | bash`
|
||||
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
@@ -595,7 +575,7 @@ WantedBy=default.target`
|
||||
acceptClients
|
||||
);
|
||||
|
||||
hydrateWireGuardConfig(
|
||||
const wgConfig = generateWireGuardConfig(
|
||||
privateKey,
|
||||
data.publicKey,
|
||||
data.subnet,
|
||||
@@ -603,6 +583,7 @@ WantedBy=default.target`
|
||||
data.endpoint,
|
||||
data.listenPort
|
||||
);
|
||||
setWgConfig(wgConfig);
|
||||
|
||||
setTunnelTypes((prev: any) => {
|
||||
return prev.map((item: any) => {
|
||||
@@ -771,7 +752,9 @@ WantedBy=default.target`
|
||||
{tunnelTypes.length > 1 && (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<span className="text-sm font-medium">{t("type")}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{t("type")}
|
||||
</span>
|
||||
</div>
|
||||
<StrategySelect
|
||||
options={tunnelTypes}
|
||||
|
||||
@@ -31,11 +31,11 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||
return "-"; // because we are not able to track the data use in a local site right now
|
||||
}
|
||||
if (mb >= 1024 * 1024) {
|
||||
return t('terabytes', {count: (mb / (1024 * 1024)).toFixed(2)});
|
||||
return t("terabytes", { count: (mb / (1024 * 1024)).toFixed(2) });
|
||||
} else if (mb >= 1024) {
|
||||
return t('gigabytes', {count: (mb / 1024).toFixed(2)});
|
||||
return t("gigabytes", { count: (mb / 1024).toFixed(2) });
|
||||
} else {
|
||||
return t('megabytes', {count: mb.toFixed(2)});
|
||||
return t("megabytes", { count: mb.toFixed(2) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||
newtVersion: site.newtVersion || undefined,
|
||||
newtUpdateAvailable: site.newtUpdateAvailable || false,
|
||||
exitNodeName: site.exitNodeName || undefined,
|
||||
exitNodeEndpoint: site.exitNodeEndpoint || undefined,
|
||||
exitNodeEndpoint: site.exitNodeEndpoint || undefined
|
||||
};
|
||||
});
|
||||
|
||||
@@ -62,8 +62,8 @@ export default async function SitesPage(props: SitesPageProps) {
|
||||
{/* <SitesSplashCard /> */}
|
||||
|
||||
<SettingsSectionTitle
|
||||
title={t('siteManageSites')}
|
||||
description={t('siteDescription')}
|
||||
title={t("siteManageSites")}
|
||||
description={t("siteDescription")}
|
||||
/>
|
||||
|
||||
<SitesTable sites={siteRows} orgId={params.orgId} />
|
||||
|
||||
Reference in New Issue
Block a user