mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-16 18:06:39 +00:00
Merge branch 'dev' into clients-user
This commit is contained in:
@@ -76,8 +76,8 @@ export default function GeneralPage() {
|
||||
.min(1, { message: t("idpClientSecretRequired") }),
|
||||
roleMapping: z.string().nullable().optional(),
|
||||
roleId: z.number().nullable().optional(),
|
||||
authUrl: z.string().url({ message: t("idpErrorAuthUrlInvalid") }),
|
||||
tokenUrl: z.string().url({ message: t("idpErrorTokenUrlInvalid") }),
|
||||
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") }),
|
||||
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") }),
|
||||
identifierPath: z.string().min(1, { message: t("idpPathRequired") }),
|
||||
emailPath: z.string().nullable().optional(),
|
||||
namePath: z.string().nullable().optional(),
|
||||
|
||||
@@ -64,13 +64,9 @@ export default function Page() {
|
||||
clientSecret: z
|
||||
.string()
|
||||
.min(1, { message: t("idpClientSecretRequired") }),
|
||||
authUrl: z
|
||||
.string()
|
||||
.url({ message: t("idpErrorAuthUrlInvalid") })
|
||||
authUrl: z.url({ message: t("idpErrorAuthUrlInvalid") })
|
||||
.optional(),
|
||||
tokenUrl: z
|
||||
.string()
|
||||
.url({ message: t("idpErrorTokenUrlInvalid") })
|
||||
tokenUrl: z.url({ message: t("idpErrorTokenUrlInvalid") })
|
||||
.optional(),
|
||||
identifierPath: z
|
||||
.string()
|
||||
|
||||
@@ -239,6 +239,7 @@ export default function ExitNodesTable({
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const nodeRow = row.original;
|
||||
const remoteExitNodeId = nodeRow.id;
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
@@ -249,6 +250,14 @@ export default function ExitNodesTable({
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<Link
|
||||
className="block w-full"
|
||||
href={`/${nodeRow.orgId}/settings/remote-exit-nodes/${remoteExitNodeId}`}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
{t("viewSettings")}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedNode(nodeRow);
|
||||
@@ -261,6 +270,14 @@ export default function ExitNodesTable({
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${nodeRow.orgId}/settings/remote-exit-nodes/${remoteExitNodeId}`}
|
||||
>
|
||||
<Button variant={"secondary"} size="sm">
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
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 { AxiosResponse } from "axios";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
PickRemoteExitNodeDefaultsResponse,
|
||||
QuickStartRemoteExitNodeResponse
|
||||
} from "@server/routers/remoteExitNode/types";
|
||||
import { useRemoteExitNodeContext } from "@app/hooks/useRemoteExitNodeContext";
|
||||
import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal";
|
||||
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";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { remoteExitNode } = useRemoteExitNodeContext();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [credentials, setCredentials] = useState<PickRemoteExitNodeDefaultsResponse | 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 response = await api.get<AxiosResponse<PickRemoteExitNodeDefaultsResponse>>(
|
||||
`/org/${orgId}/pick-remote-exit-node-defaults`
|
||||
);
|
||||
|
||||
const data = response.data.data;
|
||||
setCredentials(data);
|
||||
|
||||
await api.put<AxiosResponse<QuickStartRemoteExitNodeResponse>>(
|
||||
`/re-key/${orgId}/reGenerate-remote-exit-node-secret`,
|
||||
{
|
||||
remoteExitNodeId: remoteExitNode.remoteExitNodeId,
|
||||
secret: data.secret,
|
||||
}
|
||||
);
|
||||
|
||||
toast({
|
||||
title: t("credentialsSaved"),
|
||||
description: t("credentialsSavedDescription")
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
};
|
||||
|
||||
const getCredentials = () => {
|
||||
if (credentials) {
|
||||
return {
|
||||
Id: remoteExitNode.remoteExitNodeId,
|
||||
Secret: credentials.secret
|
||||
};
|
||||
}
|
||||
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="remote-exit-node"
|
||||
onConfirmRegenerate={handleConfirmRegenerate}
|
||||
dashboardUrl={env.app.dashboardUrl}
|
||||
credentials={getCredentials()}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function GeneralPage() {
|
||||
return <></>;
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import RemoteExitNodeProvider from "@app/providers/RemoteExitNodeProvider";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import ExitNodeInfoCard from "@app/components/ExitNodeInfoCard";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -31,6 +33,13 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: t('credentials'),
|
||||
href: "/{orgId}/settings/remote-exit-nodes/{remoteExitNodeId}/credentials"
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
@@ -39,7 +48,10 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
/>
|
||||
|
||||
<RemoteExitNodeProvider remoteExitNode={remoteExitNode}>
|
||||
<div className="space-y-6">{children}</div>
|
||||
<div className="space-y-6">
|
||||
<ExitNodeInfoCard />
|
||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
||||
</div>
|
||||
</RemoteExitNodeProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,6 @@ export default async function RemoteExitNodePage(props: {
|
||||
}) {
|
||||
const params = await props.params;
|
||||
redirect(
|
||||
`/${params.orgId}/settings/remote-exit-nodes/${params.remoteExitNodeId}/general`
|
||||
`/${params.orgId}/settings/remote-exit-nodes/${params.remoteExitNodeId}/credentials`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function Page() {
|
||||
const [dataLoaded, setDataLoaded] = useState(false);
|
||||
|
||||
const internalFormSchema = z.object({
|
||||
email: z.string().email({ message: t("emailInvalid") }),
|
||||
email: z.email({ message: t("emailInvalid") }),
|
||||
validForHours: z
|
||||
.string()
|
||||
.min(1, { message: t("inviteValidityDuration") }),
|
||||
@@ -99,16 +99,14 @@ export default function Page() {
|
||||
});
|
||||
|
||||
const googleAzureFormSchema = z.object({
|
||||
email: z.string().email({ message: t("emailInvalid") }),
|
||||
email: z.email({ message: t("emailInvalid") }),
|
||||
name: z.string().optional(),
|
||||
roleId: z.string().min(1, { message: t("accessRoleSelectPlease") })
|
||||
});
|
||||
|
||||
const genericOidcFormSchema = z.object({
|
||||
username: z.string().min(1, { message: t("usernameRequired") }),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: t("emailInvalid") })
|
||||
email: z.email({ message: t("emailInvalid") })
|
||||
.optional()
|
||||
.or(z.literal("")),
|
||||
name: z.string().optional(),
|
||||
|
||||
124
src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx
Normal file
124
src/app/[orgId]/settings/clients/[clientId]/credentials/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
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 RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal";
|
||||
import { build } from "@server/build";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip";
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import ClientInfoCard from "../../../../../components/ClientInfoCard";
|
||||
import ClientProvider from "@app/providers/ClientProvider";
|
||||
import { redirect } from "next/navigation";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { build } from "@server/build";
|
||||
|
||||
type SettingsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -30,11 +32,20 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
redirect(`/${params.orgId}/settings/clients`);
|
||||
}
|
||||
|
||||
const t = await getTranslations();
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
title: "General",
|
||||
title: t('general'),
|
||||
href: `/{orgId}/settings/clients/{clientId}/general`
|
||||
}
|
||||
},
|
||||
...(build === 'enterprise'
|
||||
? [{
|
||||
title: t('credentials'),
|
||||
href: `/{orgId}/settings/clients/{clientId}/credentials`
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
@@ -99,8 +99,12 @@ export default function Page() {
|
||||
id: z.string(),
|
||||
text: z.string()
|
||||
})
|
||||
),
|
||||
subnet: z.string().ip().min(1, {
|
||||
)
|
||||
.refine((val) => val.length > 0, {
|
||||
message: t("siteRequired")
|
||||
}),
|
||||
subnet: z.union([z.ipv4(), z.ipv6()])
|
||||
.refine((val) => val.length > 0, {
|
||||
message: t("subnetRequired")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -921,9 +921,7 @@ export default function ResourceAuthenticationPage() {
|
||||
validateTag={(
|
||||
tag
|
||||
) => {
|
||||
return z
|
||||
.string()
|
||||
.email()
|
||||
return z.email()
|
||||
.or(
|
||||
z
|
||||
.string()
|
||||
|
||||
@@ -68,9 +68,9 @@ export default function GeneralForm() {
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const [editDomainOpen, setEditDomainOpen] = useState(false);
|
||||
const {licenseStatus } = useLicenseStatusContext();
|
||||
const { licenseStatus } = useLicenseStatusContext();
|
||||
const subscriptionStatus = useSubscriptionStatusContext();
|
||||
const {user} = useUserContext();
|
||||
const { user } = useUserContext();
|
||||
|
||||
const { env } = useEnvContext();
|
||||
|
||||
@@ -102,8 +102,9 @@ export default function GeneralForm() {
|
||||
enabled: z.boolean(),
|
||||
subdomain: z.string().optional(),
|
||||
name: z.string().min(1).max(255),
|
||||
niceId: z.string().min(1).max(255).optional(),
|
||||
domainId: z.string().optional(),
|
||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||
proxyPort: z.int().min(1).max(65535).optional(),
|
||||
// enableProxy: z.boolean().optional()
|
||||
})
|
||||
.refine(
|
||||
@@ -130,6 +131,7 @@ export default function GeneralForm() {
|
||||
defaultValues: {
|
||||
enabled: resource.enabled,
|
||||
name: resource.name,
|
||||
niceId: resource.niceId,
|
||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||
domainId: resource.domainId || undefined,
|
||||
proxyPort: resource.proxyPort || undefined,
|
||||
@@ -192,6 +194,7 @@ export default function GeneralForm() {
|
||||
{
|
||||
enabled: data.enabled,
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
subdomain: data.subdomain ? toASCII(data.subdomain) : undefined,
|
||||
domainId: data.domainId,
|
||||
proxyPort: data.proxyPort,
|
||||
@@ -212,16 +215,12 @@ export default function GeneralForm() {
|
||||
});
|
||||
|
||||
if (res && res.status === 200) {
|
||||
toast({
|
||||
title: t("resourceUpdated"),
|
||||
description: t("resourceUpdatedDescription")
|
||||
});
|
||||
|
||||
const resource = res.data.data;
|
||||
const updated = res.data.data;
|
||||
|
||||
updateResource({
|
||||
enabled: data.enabled,
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
subdomain: data.subdomain,
|
||||
fullDomain: resource.fullDomain,
|
||||
proxyPort: data.proxyPort,
|
||||
@@ -230,8 +229,20 @@ export default function GeneralForm() {
|
||||
// })
|
||||
});
|
||||
|
||||
router.refresh();
|
||||
toast({
|
||||
title: t("resourceUpdated"),
|
||||
description: t("resourceUpdatedDescription")
|
||||
});
|
||||
|
||||
if (data.niceId && data.niceId !== resource?.niceId) {
|
||||
router.replace(`/${updated.orgId}/settings/resources/${data.niceId}/general`);
|
||||
} else {
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
setSaveLoading(false);
|
||||
}
|
||||
|
||||
setSaveLoading(false);
|
||||
}
|
||||
|
||||
@@ -304,6 +315,24 @@ export default function GeneralForm() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{!resource.http && (
|
||||
<>
|
||||
<FormField
|
||||
|
||||
@@ -122,11 +122,11 @@ const addTargetSchema = z
|
||||
.object({
|
||||
ip: z.string().refine(isTargetValid),
|
||||
method: z.string().nullable(),
|
||||
port: z.coerce.number().int().positive(),
|
||||
siteId: z
|
||||
.number()
|
||||
.int()
|
||||
.positive({ message: "You must select a site for a target." }),
|
||||
port: z.coerce.number<number>().int().positive(),
|
||||
siteId: z.int()
|
||||
.positive({
|
||||
error: "You must select a site for a target."
|
||||
}),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z
|
||||
.enum(["exact", "prefix", "regex"])
|
||||
@@ -137,7 +137,7 @@ const addTargetSchema = z
|
||||
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
||||
.optional()
|
||||
.nullable(),
|
||||
priority: z.number().int().min(1).max(1000).optional()
|
||||
priority: z.int().min(1).max(1000).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -169,7 +169,7 @@ const addTargetSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Invalid path configuration"
|
||||
error: "Invalid path configuration"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
@@ -185,7 +185,7 @@ const addTargetSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Invalid rewrite path configuration"
|
||||
error: "Invalid rewrite path configuration"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -292,7 +292,7 @@ export default function ReverseProxyTargets(props: {
|
||||
.array(z.object({ name: z.string(), value: z.string() }))
|
||||
.nullable(),
|
||||
proxyProtocol: z.boolean().optional(),
|
||||
proxyProtocolVersion: z.number().int().min(1).max(2).optional()
|
||||
proxyProtocolVersion: z.int().min(1).max(2).optional()
|
||||
});
|
||||
|
||||
const tlsSettingsSchema = z.object({
|
||||
@@ -512,9 +512,18 @@ export default function ReverseProxyTargets(props: {
|
||||
port: target.port,
|
||||
enabled: target.enabled,
|
||||
hcEnabled: target.hcEnabled,
|
||||
hcPath: target.hcPath,
|
||||
hcInterval: target.hcInterval,
|
||||
hcTimeout: target.hcTimeout
|
||||
hcPath: target.hcPath || null,
|
||||
hcScheme: target.hcScheme || null,
|
||||
hcHostname: target.hcHostname || null,
|
||||
hcPort: target.hcPort || null,
|
||||
hcInterval: target.hcInterval || null,
|
||||
hcTimeout: target.hcTimeout || null,
|
||||
hcHeaders: target.hcHeaders || null,
|
||||
hcFollowRedirects: target.hcFollowRedirects || null,
|
||||
hcMethod: target.hcMethod || null,
|
||||
hcStatus: target.hcStatus || null,
|
||||
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
||||
hcMode: target.hcMode || null
|
||||
};
|
||||
|
||||
// Only include path-related fields for HTTP resources
|
||||
@@ -718,7 +727,9 @@ export default function ReverseProxyTargets(props: {
|
||||
hcHeaders: target.hcHeaders || null,
|
||||
hcFollowRedirects: target.hcFollowRedirects || null,
|
||||
hcMethod: target.hcMethod || null,
|
||||
hcStatus: target.hcStatus || null
|
||||
hcStatus: target.hcStatus || null,
|
||||
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
||||
hcMode: target.hcMode || null
|
||||
};
|
||||
|
||||
// Only include path-related fields for HTTP resources
|
||||
@@ -1822,6 +1833,7 @@ export default function ReverseProxyTargets(props: {
|
||||
30
|
||||
}}
|
||||
onChanges={async (config) => {
|
||||
console.log("here");
|
||||
if (selectedTargetForHealthCheck) {
|
||||
console.log(config);
|
||||
updateTargetHealthCheck(
|
||||
|
||||
@@ -93,7 +93,7 @@ const addRuleSchema = z.object({
|
||||
action: z.enum(["ACCEPT", "DROP", "PASS"]),
|
||||
match: z.string(),
|
||||
value: z.string(),
|
||||
priority: z.coerce.number().int().optional()
|
||||
priority: z.coerce.number<number>().int().optional()
|
||||
});
|
||||
|
||||
type LocalRule = ArrayElement<ListResourceRulesResponse["rules"]> & {
|
||||
@@ -440,9 +440,7 @@ export default function ResourceRules(props: {
|
||||
type="number"
|
||||
onClick={(e) => e.currentTarget.focus()}
|
||||
onBlur={(e) => {
|
||||
const parsed = z.coerce
|
||||
.number()
|
||||
.int()
|
||||
const parsed = z.int()
|
||||
.optional()
|
||||
.safeParse(e.target.value);
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ const httpResourceFormSchema = z.object({
|
||||
|
||||
const tcpUdpResourceFormSchema = z.object({
|
||||
protocol: z.string(),
|
||||
proxyPort: z.number().int().min(1).max(65535)
|
||||
proxyPort: z.int().min(1).max(65535)
|
||||
// enableProxy: z.boolean().default(false)
|
||||
});
|
||||
|
||||
@@ -136,8 +136,8 @@ const addTargetSchema = z
|
||||
.object({
|
||||
ip: z.string().refine(isTargetValid),
|
||||
method: z.string().nullable(),
|
||||
port: z.coerce.number().int().positive(),
|
||||
siteId: z.number().int().positive(),
|
||||
port: z.coerce.number<number>().int().positive(),
|
||||
siteId: z.int().positive(),
|
||||
path: z.string().optional().nullable(),
|
||||
pathMatchType: z
|
||||
.enum(["exact", "prefix", "regex"])
|
||||
@@ -148,7 +148,7 @@ const addTargetSchema = z
|
||||
.enum(["exact", "prefix", "regex", "stripPrefix"])
|
||||
.optional()
|
||||
.nullable(),
|
||||
priority: z.number().int().min(1).max(1000).optional()
|
||||
priority: z.int().min(1).max(1000).optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -180,7 +180,7 @@ const addTargetSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Invalid path configuration"
|
||||
error: "Invalid path configuration"
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
@@ -196,7 +196,7 @@ const addTargetSchema = z
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Invalid rewrite path configuration"
|
||||
error: "Invalid rewrite path configuration"
|
||||
}
|
||||
);
|
||||
|
||||
@@ -574,7 +574,9 @@ export default function Page() {
|
||||
hcPort: target.hcPort || null,
|
||||
hcFollowRedirects:
|
||||
target.hcFollowRedirects || null,
|
||||
hcStatus: target.hcStatus || null
|
||||
hcStatus: target.hcStatus || null,
|
||||
hcUnhealthyInterval: target.hcUnhealthyInterval || null,
|
||||
hcMode: target.hcMode || null
|
||||
};
|
||||
|
||||
// Only include path-related fields for HTTP resources
|
||||
|
||||
193
src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx
Normal file
193
src/app/[orgId]/settings/sites/[niceId]/credentials/page.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
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 { PickSiteDefaultsResponse } from "@server/routers/site";
|
||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||
import { generateKeypair } from "../wireguardConfig";
|
||||
import RegenerateCredentialsModal from "@app/components/RegenerateCredentialsModal";
|
||||
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";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
const { orgId } = useParams();
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { site } = useSiteContext();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [siteDefaults, setSiteDefaults] = useState<PickSiteDefaultsResponse | null>(null);
|
||||
const [wgConfig, setWgConfig] = useState("");
|
||||
const [publicKey, setPublicKey] = useState("");
|
||||
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const isSecurityFeatureDisabled = () => {
|
||||
const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked();
|
||||
const isSaasNotSubscribed =
|
||||
build === "saas" && !subscription?.isSubscribed();
|
||||
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;
|
||||
};
|
||||
|
||||
const handleConfirmRegenerate = async () => {
|
||||
let generatedPublicKey = "";
|
||||
let generatedWgConfig = "";
|
||||
|
||||
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);
|
||||
|
||||
// generate config with the fetched data
|
||||
generatedWgConfig = hydrateWireGuardConfig(
|
||||
generatedKeypair.privateKey,
|
||||
data.publicKey,
|
||||
data.subnet,
|
||||
data.address,
|
||||
data.endpoint,
|
||||
data.listenPort
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
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 getCredentials = () => {
|
||||
if (site?.type === "wireguard" && wgConfig) {
|
||||
return { wgConfig };
|
||||
}
|
||||
if (site?.type === "newt" && siteDefaults) {
|
||||
return {
|
||||
Id: siteDefaults.newtId,
|
||||
Secret: siteDefaults.newtSecret
|
||||
};
|
||||
}
|
||||
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={getCredentialType()}
|
||||
onConfirmRegenerate={handleConfirmRegenerate}
|
||||
dashboardUrl={env.app.dashboardUrl}
|
||||
credentials={getCredentials()}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { toast, useToast } from "@app/hooks/useToast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
SettingsContainer,
|
||||
@@ -36,7 +36,8 @@ import Link from "next/link";
|
||||
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string().nonempty("Name is required"),
|
||||
dockerSocketEnabled: z.boolean().optional()
|
||||
niceId: z.string().min(1).max(255).optional(),
|
||||
dockerSocketEnabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
@@ -46,20 +47,19 @@ export default function GeneralPage() {
|
||||
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient(useEnvContext());
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(null);
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
defaultValues: {
|
||||
name: site?.name,
|
||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false
|
||||
niceId: site?.niceId || "",
|
||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false,
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
@@ -67,31 +67,34 @@ export default function GeneralPage() {
|
||||
async function onSubmit(data: GeneralFormValues) {
|
||||
setLoading(true);
|
||||
|
||||
await api
|
||||
.post(`/site/${site?.siteId}`, {
|
||||
try {
|
||||
await api.post(`/site/${site?.siteId}`, {
|
||||
name: data.name,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled
|
||||
})
|
||||
.catch((e) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("siteErrorUpdate"),
|
||||
description: formatAxiosError(
|
||||
e,
|
||||
t("siteErrorUpdateDescription")
|
||||
)
|
||||
});
|
||||
niceId: data.niceId,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||
});
|
||||
|
||||
updateSite({
|
||||
name: data.name,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled
|
||||
});
|
||||
updateSite({
|
||||
name: data.name,
|
||||
niceId: data.niceId,
|
||||
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t("siteUpdated"),
|
||||
description: t("siteUpdatedDescription")
|
||||
});
|
||||
if (data.niceId && data.niceId !== site?.niceId) {
|
||||
router.replace(`/${site?.orgId}/settings/sites/${data.niceId}/general`);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: t("siteUpdated"),
|
||||
description: t("siteUpdatedDescription")
|
||||
});
|
||||
} catch (e) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("siteErrorUpdate"),
|
||||
description: formatAxiosError(e, t("siteErrorUpdateDescription"))
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
|
||||
@@ -132,6 +135,24 @@ export default function GeneralPage() {
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
|
||||
{site && site.type === "newt" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import SiteInfoCard from "../../../../../components/SiteInfoCard";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { build } from "@server/build";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
@@ -35,14 +36,23 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||
const navItems = [
|
||||
{
|
||||
title: t('general'),
|
||||
href: "/{orgId}/settings/sites/{niceId}/general"
|
||||
}
|
||||
href: `/${params.orgId}/settings/sites/${params.niceId}/general`,
|
||||
},
|
||||
...(site.type !== 'local' && build === 'enterprise'
|
||||
? [
|
||||
{
|
||||
title: t('credentials'),
|
||||
href: `/${params.orgId}/settings/sites/${params.niceId}/credentials`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t('siteSetting', {siteName: site?.name})}
|
||||
title={t('siteSetting', { siteName: site?.name })}
|
||||
description={t('siteSettingDescription')}
|
||||
/>
|
||||
|
||||
|
||||
@@ -61,8 +61,8 @@ export default function GeneralPage() {
|
||||
name: z.string().min(2, { message: t('nameMin', {len: 2}) }),
|
||||
clientId: z.string().min(1, { message: t('idpClientIdRequired') }),
|
||||
clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }),
|
||||
authUrl: z.string().url({ message: t('idpErrorAuthUrlInvalid') }),
|
||||
tokenUrl: z.string().url({ message: t('idpErrorTokenUrlInvalid') }),
|
||||
authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }),
|
||||
tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }),
|
||||
identifierPath: z
|
||||
.string()
|
||||
.min(1, { message: t('idpPathRequired') }),
|
||||
|
||||
@@ -52,8 +52,8 @@ export default function Page() {
|
||||
type: z.enum(["oidc"]),
|
||||
clientId: z.string().min(1, { message: t('idpClientIdRequired') }),
|
||||
clientSecret: z.string().min(1, { message: t('idpClientSecretRequired') }),
|
||||
authUrl: z.string().url({ message: t('idpErrorAuthUrlInvalid') }),
|
||||
tokenUrl: z.string().url({ message: t('idpErrorTokenUrlInvalid') }),
|
||||
authUrl: z.url({ message: t('idpErrorAuthUrlInvalid') }),
|
||||
tokenUrl: z.url({ message: t('idpErrorTokenUrlInvalid') }),
|
||||
identifierPath: z
|
||||
.string()
|
||||
.min(1, { message: t('idpPathRequired') }),
|
||||
|
||||
@@ -47,7 +47,7 @@ import { cleanRedirect } from "@app/lib/cleanRedirect";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const requestSchema = z.object({
|
||||
email: z.string().email()
|
||||
email: z.email()
|
||||
});
|
||||
|
||||
export type ResetPasswordFormProps = {
|
||||
@@ -88,7 +88,7 @@ export default function ResetPasswordForm({
|
||||
|
||||
const formSchema = z
|
||||
.object({
|
||||
email: z.string().email({ message: t('emailInvalid') }),
|
||||
email: z.email({ message: t('emailInvalid') }),
|
||||
token: z.string().min(8, { message: t('tokenInvalid') }),
|
||||
password: passwordSchema,
|
||||
confirmPassword: passwordSchema
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Toaster } from "@app/components/ui/toaster";
|
||||
import { build } from "@server/build";
|
||||
import { TopLoader } from "@app/components/Toploader";
|
||||
import Script from "next/script";
|
||||
import { ReactQueryProvider } from "@app/components/react-query-provider";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Dashboard - ${process.env.BRANDING_APP_NAME || "Pangolin"}`,
|
||||
@@ -94,38 +95,40 @@ export default async function RootLayout({
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
)}
|
||||
<NextIntlClientProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ThemeDataProvider colors={loadBrandingColors()}>
|
||||
<EnvProvider env={pullEnv()}>
|
||||
<LicenseStatusProvider
|
||||
licenseStatus={licenseStatus}
|
||||
>
|
||||
<SupportStatusProvider
|
||||
supporterStatus={supporterData}
|
||||
<ReactQueryProvider>
|
||||
<NextIntlClientProvider>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<ThemeDataProvider colors={loadBrandingColors()}>
|
||||
<EnvProvider env={pullEnv()}>
|
||||
<LicenseStatusProvider
|
||||
licenseStatus={licenseStatus}
|
||||
>
|
||||
{/* Main content */}
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<SplashImage>
|
||||
<SupportStatusProvider
|
||||
supporterStatus={supporterData}
|
||||
>
|
||||
{/* Main content */}
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 overflow-auto">
|
||||
<SplashImage>
|
||||
<LicenseViolation />
|
||||
{children}
|
||||
</SplashImage>
|
||||
<LicenseViolation />
|
||||
{children}
|
||||
</SplashImage>
|
||||
<LicenseViolation />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SupportStatusProvider>
|
||||
</LicenseStatusProvider>
|
||||
<Toaster />
|
||||
</EnvProvider>
|
||||
</ThemeDataProvider>
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</SupportStatusProvider>
|
||||
</LicenseStatusProvider>
|
||||
<Toaster />
|
||||
</EnvProvider>
|
||||
</ThemeDataProvider>
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</ReactQueryProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user