mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
Various fixes for rc
This commit is contained in:
@@ -1744,7 +1744,6 @@
|
|||||||
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
|
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
|
||||||
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
|
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
|
||||||
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
|
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
|
||||||
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
|
|
||||||
"idpDisabled": "Identity providers are disabled.",
|
"idpDisabled": "Identity providers are disabled.",
|
||||||
"orgAuthPageDisabled": "Organization auth page is disabled.",
|
"orgAuthPageDisabled": "Organization auth page is disabled.",
|
||||||
"domainRestartedDescription": "Domain verification restarted successfully",
|
"domainRestartedDescription": "Domain verification restarted successfully",
|
||||||
@@ -2040,5 +2039,7 @@
|
|||||||
"version2": "Version 2",
|
"version2": "Version 2",
|
||||||
"versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible.",
|
"versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible.",
|
||||||
"warning": "Warning",
|
"warning": "Warning",
|
||||||
"proxyProtocolWarning": "Your backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections. Make sure to configure your backend to trust Proxy Protocol headers from Traefik."
|
"proxyProtocolWarning": "Your backend application must be configured to accept Proxy Protocol connections. If your backend doesn't support Proxy Protocol, enabling this will break all connections. Make sure to configure your backend to trust Proxy Protocol headers from Traefik.",
|
||||||
|
"restarting": "Restarting...",
|
||||||
|
"manual": "Manual"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export async function logRequestAudit(
|
|||||||
try {
|
try {
|
||||||
if (data.orgId) {
|
if (data.orgId) {
|
||||||
const retentionDays = await getRetentionDays(data.orgId);
|
const retentionDays = await getRetentionDays(data.orgId);
|
||||||
if (retentionDays === 0) {
|
if (retentionDays == 0) {
|
||||||
// do not log
|
// do not log
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding";
|
|||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { response } from "@server/lib/response";
|
import { response } from "@server/lib/response";
|
||||||
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
|
||||||
import { logAccessAudit } from "#private/lib/logAccessAudit";
|
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
|
||||||
|
|
||||||
const getExchangeTokenParams = z
|
const getExchangeTokenParams = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default async function migration() {
|
|||||||
try {
|
try {
|
||||||
await db.execute(sql`BEGIN`);
|
await db.execute(sql`BEGIN`);
|
||||||
|
|
||||||
await db.execute(sql`UPDATE "resourceRules" SET "match" = "COUNTRY" WHERE "match" = "GEOIP"`);
|
await db.execute(sql`UPDATE "resourceRules" SET "match" = 'COUNTRY' WHERE "match" = 'GEOIP'`);
|
||||||
|
|
||||||
await db.execute(sql`
|
await db.execute(sql`
|
||||||
CREATE TABLE "accessAuditLog" (
|
CREATE TABLE "accessAuditLog" (
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default async function migration() {
|
|||||||
|
|
||||||
db.transaction(() => {
|
db.transaction(() => {
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`UPDATE resourceRules SET match = "COUNTRY" WHERE match = "GEOIP"`
|
`UPDATE 'resourceRules' SET 'match' = 'COUNTRY' WHERE 'match' = 'GEOIP'`
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
@@ -155,7 +155,7 @@ export default async function migration() {
|
|||||||
).run();
|
).run();
|
||||||
|
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`INSERT INTO '__new_resources'("resourceId", "resourceGuid", "orgId", "niceId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", "skipToIdpId", "headers", "proxyProtocol", "proxyProtocolVersion") SELECT "resourceId", "resourceGuid", "orgId", "niceId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", "skipToIdpId", "headers", "proxyProtocol", "proxyProtocolVersion" FROM 'resources';`
|
`INSERT INTO '__new_resources'("resourceId", "resourceGuid", "orgId", "niceId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", "skipToIdpId", "headers") SELECT "resourceId", "resourceGuid", "orgId", "niceId", "name", "subdomain", "fullDomain", "domainId", "ssl", "blockAccess", "sso", "http", "protocol", "proxyPort", "emailWhitelistEnabled", "applyRules", "enabled", "stickySession", "tlsServerName", "setHostHeader", "enableProxy", "skipToIdpId", "headers" FROM 'resources';`
|
||||||
).run();
|
).run();
|
||||||
db.prepare(`DROP TABLE 'resources';`).run();
|
db.prepare(`DROP TABLE 'resources';`).run();
|
||||||
db.prepare(
|
db.prepare(
|
||||||
|
|||||||
@@ -12,93 +12,98 @@ import { useDomain } from "@app/contexts/domainContext";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function DomainSettingsPage() {
|
export default function DomainSettingsPage() {
|
||||||
const { domain, orgId } = useDomain();
|
const { domain, orgId } = useDomain();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(new Set());
|
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(
|
||||||
const t = useTranslations();
|
new Set()
|
||||||
|
);
|
||||||
|
const t = useTranslations();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const refreshData = async () => {
|
const refreshData = async () => {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: t("error"),
|
title: t("error"),
|
||||||
description: t("refreshError"),
|
description: t("refreshError"),
|
||||||
variant: "destructive",
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const restartDomain = async (domainId: string) => {
|
||||||
|
setRestartingDomains((prev) => new Set(prev).add(domainId));
|
||||||
|
try {
|
||||||
|
await api.post(`/org/${orgId}/domain/${domainId}/restart`);
|
||||||
|
toast({
|
||||||
|
title: t("success"),
|
||||||
|
description: t("domainRestartedDescription", {
|
||||||
|
fallback: "Domain verification restarted successfully"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
refreshData();
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: formatAxiosError(e),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setRestartingDomains((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(domainId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!domain) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const restartDomain = async (domainId: string) => {
|
const isRestarting = restartingDomains.has(domain.domainId);
|
||||||
setRestartingDomains((prev) => new Set(prev).add(domainId));
|
|
||||||
try {
|
|
||||||
await api.post(`/org/${orgId}/domain/${domainId}/restart`);
|
|
||||||
toast({
|
|
||||||
title: t("success"),
|
|
||||||
description: t("domainRestartedDescription", {
|
|
||||||
fallback: "Domain verification restarted successfully",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
refreshData();
|
|
||||||
} catch (e) {
|
|
||||||
toast({
|
|
||||||
title: t("error"),
|
|
||||||
description: formatAxiosError(e),
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setRestartingDomains((prev) => {
|
|
||||||
const newSet = new Set(prev);
|
|
||||||
newSet.delete(domainId);
|
|
||||||
return newSet;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!domain) {
|
return (
|
||||||
return null;
|
<>
|
||||||
}
|
<div className="flex justify-between">
|
||||||
|
<SettingsSectionTitle
|
||||||
const isRestarting = restartingDomains.has(domain.domainId);
|
title={domain.baseDomain}
|
||||||
|
description={t("domainSettingDescription")}
|
||||||
return (
|
/>
|
||||||
<>
|
{env.flags.usePangolinDns && (
|
||||||
<div className="flex justify-between">
|
<Button
|
||||||
<SettingsSectionTitle
|
variant="outline"
|
||||||
title={domain.baseDomain}
|
onClick={() => restartDomain(domain.domainId)}
|
||||||
description={t("domainSettingDescription")}
|
disabled={isRestarting}
|
||||||
/>
|
>
|
||||||
<Button
|
{isRestarting ? (
|
||||||
variant="outline"
|
<>
|
||||||
onClick={() => restartDomain(domain.domainId)}
|
<RefreshCw
|
||||||
disabled={isRestarting}
|
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||||
>
|
/>
|
||||||
{isRestarting ? (
|
{t("restarting", { fallback: "Restarting..." })}
|
||||||
<>
|
</>
|
||||||
<RefreshCw
|
) : (
|
||||||
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
<>
|
||||||
/>
|
<RefreshCw
|
||||||
{t("restarting", { fallback: "Restarting..." })}
|
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||||
</>
|
/>
|
||||||
) : (
|
{t("restart", { fallback: "Restart" })}
|
||||||
<>
|
</>
|
||||||
<RefreshCw
|
)}
|
||||||
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
</Button>
|
||||||
/>
|
)}
|
||||||
{t("restart", { fallback: "Restart" })}
|
</div>
|
||||||
</>
|
<div className="space-y-6">
|
||||||
)}
|
<DomainInfoCard orgId={orgId} domainId={domain.domainId} />
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
</>
|
||||||
<div className="space-y-6">
|
);
|
||||||
<DomainInfoCard orgId={orgId} domainId={domain.domainId} />
|
}
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ export default function GeneralPage() {
|
|||||||
<ChevronDown className="h-4 w-4" />
|
<ChevronDown className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-full">
|
<DropdownMenuContent>
|
||||||
{LOG_RETENTION_OPTIONS.filter(
|
{LOG_RETENTION_OPTIONS.filter(
|
||||||
(option) => {
|
(option) => {
|
||||||
if (
|
if (
|
||||||
@@ -627,243 +627,256 @@ export default function GeneralPage() {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
|
{build !== "oss" && (
|
||||||
|
<SettingsSection>
|
||||||
|
<SettingsSectionHeader>
|
||||||
|
<SettingsSectionTitle>
|
||||||
|
{t("securitySettings")}
|
||||||
|
</SettingsSectionTitle>
|
||||||
|
<SettingsSectionDescription>
|
||||||
|
{t("securitySettingsDescription")}
|
||||||
|
</SettingsSectionDescription>
|
||||||
|
</SettingsSectionHeader>
|
||||||
|
<SettingsSectionBody>
|
||||||
|
<SecurityFeaturesAlert />
|
||||||
|
|
||||||
{/* Security Settings Section */}
|
<SettingsSectionForm>
|
||||||
<SettingsSection>
|
<Form {...form}>
|
||||||
<SettingsSectionHeader>
|
<form
|
||||||
<SettingsSectionTitle>
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
{t("securitySettings")}
|
className="space-y-4"
|
||||||
</SettingsSectionTitle>
|
id="security-settings-form"
|
||||||
<SettingsSectionDescription>
|
>
|
||||||
{t("securitySettingsDescription")}
|
<FormField
|
||||||
</SettingsSectionDescription>
|
control={form.control}
|
||||||
</SettingsSectionHeader>
|
name="requireTwoFactor"
|
||||||
<SettingsSectionBody>
|
render={({ field }) => {
|
||||||
<SecurityFeaturesAlert />
|
const isDisabled =
|
||||||
|
isSecurityFeatureDisabled();
|
||||||
|
|
||||||
<SettingsSectionForm>
|
return (
|
||||||
<Form {...form}>
|
<FormItem className="col-span-2">
|
||||||
<form
|
<div className="flex items-center gap-2">
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
<FormControl>
|
||||||
className="space-y-4"
|
<SwitchInput
|
||||||
id="security-settings-form"
|
id="require-two-factor"
|
||||||
>
|
defaultChecked={
|
||||||
<FormField
|
field.value ||
|
||||||
control={form.control}
|
false
|
||||||
name="requireTwoFactor"
|
}
|
||||||
render={({ field }) => {
|
label={t(
|
||||||
const isDisabled =
|
"requireTwoFactorForAllUsers"
|
||||||
isSecurityFeatureDisabled();
|
)}
|
||||||
|
disabled={
|
||||||
|
isDisabled
|
||||||
|
}
|
||||||
|
onCheckedChange={(
|
||||||
|
val
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!isDisabled
|
||||||
|
) {
|
||||||
|
form.setValue(
|
||||||
|
"requireTwoFactor",
|
||||||
|
val
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"requireTwoFactorDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="maxSessionLengthHours"
|
||||||
|
render={({ field }) => {
|
||||||
|
const isDisabled =
|
||||||
|
isSecurityFeatureDisabled();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<div className="flex items-center gap-2">
|
<FormLabel>
|
||||||
|
{t("maxSessionLength")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SwitchInput
|
<Select
|
||||||
id="require-two-factor"
|
value={
|
||||||
defaultChecked={
|
field.value?.toString() ||
|
||||||
field.value ||
|
"null"
|
||||||
false
|
|
||||||
}
|
}
|
||||||
label={t(
|
onValueChange={(
|
||||||
"requireTwoFactorForAllUsers"
|
value
|
||||||
)}
|
|
||||||
disabled={
|
|
||||||
isDisabled
|
|
||||||
}
|
|
||||||
onCheckedChange={(
|
|
||||||
val
|
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (
|
||||||
!isDisabled
|
!isDisabled
|
||||||
) {
|
) {
|
||||||
|
const numValue =
|
||||||
|
value ===
|
||||||
|
"null"
|
||||||
|
? null
|
||||||
|
: parseInt(
|
||||||
|
value,
|
||||||
|
10
|
||||||
|
);
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"requireTwoFactor",
|
"maxSessionLengthHours",
|
||||||
val
|
numValue
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
disabled={
|
||||||
|
isDisabled
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"selectSessionLength"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{SESSION_LENGTH_OPTIONS.map(
|
||||||
|
(
|
||||||
|
option
|
||||||
|
) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
option.value ===
|
||||||
|
null
|
||||||
|
? "null"
|
||||||
|
: option.value.toString()
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
option.value ===
|
||||||
|
null
|
||||||
|
? "null"
|
||||||
|
: option.value.toString()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
option.labelKey
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</div>
|
|
||||||
<FormMessage />
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"requireTwoFactorDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="maxSessionLengthHours"
|
|
||||||
render={({ field }) => {
|
|
||||||
const isDisabled =
|
|
||||||
isSecurityFeatureDisabled();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItem className="col-span-2">
|
|
||||||
<FormLabel>
|
|
||||||
{t("maxSessionLength")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
value={
|
|
||||||
field.value?.toString() ||
|
|
||||||
"null"
|
|
||||||
}
|
|
||||||
onValueChange={(
|
|
||||||
value
|
|
||||||
) => {
|
|
||||||
if (!isDisabled) {
|
|
||||||
const numValue =
|
|
||||||
value ===
|
|
||||||
"null"
|
|
||||||
? null
|
|
||||||
: parseInt(
|
|
||||||
value,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"maxSessionLengthHours",
|
|
||||||
numValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"selectSessionLength"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{SESSION_LENGTH_OPTIONS.map(
|
|
||||||
(option) => (
|
|
||||||
<SelectItem
|
|
||||||
key={
|
|
||||||
option.value ===
|
|
||||||
null
|
|
||||||
? "null"
|
|
||||||
: option.value.toString()
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
option.value ===
|
|
||||||
null
|
|
||||||
? "null"
|
|
||||||
: option.value.toString()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
option.labelKey
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"maxSessionLengthDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="passwordExpiryDays"
|
|
||||||
render={({ field }) => {
|
|
||||||
const isDisabled =
|
|
||||||
isSecurityFeatureDisabled();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormItem className="col-span-2">
|
|
||||||
<FormLabel>
|
|
||||||
{t("passwordExpiryDays")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Select
|
|
||||||
value={
|
|
||||||
field.value?.toString() ||
|
|
||||||
"null"
|
|
||||||
}
|
|
||||||
onValueChange={(
|
|
||||||
value
|
|
||||||
) => {
|
|
||||||
if (!isDisabled) {
|
|
||||||
const numValue =
|
|
||||||
value ===
|
|
||||||
"null"
|
|
||||||
? null
|
|
||||||
: parseInt(
|
|
||||||
value,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
form.setValue(
|
|
||||||
"passwordExpiryDays",
|
|
||||||
numValue
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isDisabled}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={t(
|
|
||||||
"selectPasswordExpiry"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{PASSWORD_EXPIRY_OPTIONS.map(
|
|
||||||
(option) => (
|
|
||||||
<SelectItem
|
|
||||||
key={
|
|
||||||
option.value ===
|
|
||||||
null
|
|
||||||
? "null"
|
|
||||||
: option.value.toString()
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
option.value ===
|
|
||||||
null
|
|
||||||
? "null"
|
|
||||||
: option.value.toString()
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
option.labelKey
|
|
||||||
)}
|
|
||||||
</SelectItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
{t(
|
<FormDescription>
|
||||||
"editPasswordExpiryDescription"
|
{t(
|
||||||
)}
|
"maxSessionLengthDescription"
|
||||||
</FormDescription>
|
)}
|
||||||
</FormItem>
|
</FormDescription>
|
||||||
);
|
</FormItem>
|
||||||
}}
|
);
|
||||||
/>
|
}}
|
||||||
</form>
|
/>
|
||||||
</Form>
|
<FormField
|
||||||
</SettingsSectionForm>
|
control={form.control}
|
||||||
</SettingsSectionBody>
|
name="passwordExpiryDays"
|
||||||
</SettingsSection>
|
render={({ field }) => {
|
||||||
|
const isDisabled =
|
||||||
|
isSecurityFeatureDisabled();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"passwordExpiryDays"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
field.value?.toString() ||
|
||||||
|
"null"
|
||||||
|
}
|
||||||
|
onValueChange={(
|
||||||
|
value
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
!isDisabled
|
||||||
|
) {
|
||||||
|
const numValue =
|
||||||
|
value ===
|
||||||
|
"null"
|
||||||
|
? null
|
||||||
|
: parseInt(
|
||||||
|
value,
|
||||||
|
10
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"passwordExpiryDays",
|
||||||
|
numValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
isDisabled
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"selectPasswordExpiry"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PASSWORD_EXPIRY_OPTIONS.map(
|
||||||
|
(
|
||||||
|
option
|
||||||
|
) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
option.value ===
|
||||||
|
null
|
||||||
|
? "null"
|
||||||
|
: option.value.toString()
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
option.value ===
|
||||||
|
null
|
||||||
|
? "null"
|
||||||
|
: option.value.toString()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
option.labelKey
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
{t(
|
||||||
|
"editPasswordExpiryDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</SettingsSectionForm>
|
||||||
|
</SettingsSectionBody>
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
|
||||||
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
|
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,15 @@ type Props = {
|
|||||||
records: DNSRecordRow[];
|
records: DNSRecordRow[];
|
||||||
domainId: string;
|
domainId: string;
|
||||||
isRefreshing?: boolean;
|
isRefreshing?: boolean;
|
||||||
|
type: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function DNSRecordsTable({ records, domainId, isRefreshing }: Props) {
|
export default function DNSRecordsTable({
|
||||||
|
records,
|
||||||
|
domainId,
|
||||||
|
isRefreshing,
|
||||||
|
type
|
||||||
|
}: Props) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const columns: ColumnDef<DNSRecordRow>[] = [
|
const columns: ColumnDef<DNSRecordRow>[] = [
|
||||||
@@ -28,56 +34,31 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro
|
|||||||
accessorKey: "baseDomain",
|
accessorKey: "baseDomain",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div>{t("recordName", { fallback: "Record name" })}</div>
|
||||||
>
|
|
||||||
{t("recordName", { fallback: "Record name" })}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const baseDomain = row.original.baseDomain;
|
const baseDomain = row.original.baseDomain;
|
||||||
return (
|
return <div>{baseDomain || "-"}</div>;
|
||||||
<div>
|
|
||||||
{baseDomain || "-"}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "recordType",
|
accessorKey: "recordType",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return <div>{t("type")}</div>;
|
||||||
<div
|
|
||||||
>
|
|
||||||
{t("type")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const type = row.original.recordType;
|
const type = row.original.recordType;
|
||||||
return (
|
return <div className="">{type}</div>;
|
||||||
<div className="">
|
|
||||||
{type}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "ttl",
|
accessorKey: "ttl",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return <div>{t("TTL")}</div>;
|
||||||
<div
|
|
||||||
>
|
|
||||||
{t("TTL")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return (
|
return <div>{t("auto")}</div>;
|
||||||
<div>
|
|
||||||
{t("auto")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -87,44 +68,39 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro
|
|||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const value = row.original.value;
|
const value = row.original.value;
|
||||||
return (
|
return <div>{value}</div>;
|
||||||
<div>
|
|
||||||
{value}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "verified",
|
accessorKey: "verified",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return <div>{t("status")}</div>;
|
||||||
<div
|
|
||||||
>
|
|
||||||
{t("status")}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const verified = row.original.verified;
|
const verified = row.original.verified;
|
||||||
return (
|
return verified ? (
|
||||||
verified ? (
|
type === "wildcard" ? (
|
||||||
<Badge variant="green">{t("verified")}</Badge>
|
<Badge variant="outlinePrimary">
|
||||||
) : (
|
{t("manual", { fallback: "Manual" })}
|
||||||
<Badge variant="yellow">
|
|
||||||
{t("pending", { fallback: "Pending" })}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="green">{t("verified")}</Badge>
|
||||||
)
|
)
|
||||||
|
) : (
|
||||||
|
<Badge variant="yellow">
|
||||||
|
{t("pending", { fallback: "Pending" })}
|
||||||
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DNSRecordsDataTable
|
<DNSRecordsDataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={records}
|
data={records}
|
||||||
isRefreshing={isRefreshing}
|
isRefreshing={isRefreshing}
|
||||||
|
type={type}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ import {
|
|||||||
import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Badge } from "./ui/badge";
|
import { Badge } from "./ui/badge";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
|
||||||
type TabFilter = {
|
type TabFilter = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -55,6 +56,7 @@ type DNSRecordsDataTableProps<TData, TValue> = {
|
|||||||
defaultTab?: string;
|
defaultTab?: string;
|
||||||
persistPageSize?: boolean | string;
|
persistPageSize?: boolean | string;
|
||||||
defaultPageSize?: number;
|
defaultPageSize?: number;
|
||||||
|
type?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DNSRecordsDataTable<TData, TValue>({
|
export function DNSRecordsDataTable<TData, TValue>({
|
||||||
@@ -68,7 +70,7 @@ export function DNSRecordsDataTable<TData, TValue>({
|
|||||||
defaultSort,
|
defaultSort,
|
||||||
tabs,
|
tabs,
|
||||||
defaultTab,
|
defaultTab,
|
||||||
|
type
|
||||||
}: DNSRecordsDataTableProps<TData, TValue>) {
|
}: DNSRecordsDataTableProps<TData, TValue>) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -97,12 +99,9 @@ export function DNSRecordsDataTable<TData, TValue>({
|
|||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto max-w-12xl">
|
<div className="container mx-auto max-w-12xl">
|
||||||
<Card>
|
<Card>
|
||||||
@@ -112,28 +111,31 @@ export function DNSRecordsDataTable<TData, TValue>({
|
|||||||
<h1 className="font-bold">{t("dnsRecord")}</h1>
|
<h1 className="font-bold">{t("dnsRecord")}</h1>
|
||||||
<Badge variant="secondary">{t("required")}</Badge>
|
<Badge variant="secondary">{t("required")}</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Link href="https://docs.pangolin.net/self-host/dns-and-networking">
|
||||||
variant="outline"
|
<Button variant="outline">
|
||||||
>
|
<ExternalLink className="h-4 w-4 mr-1" />
|
||||||
<ExternalLink className="h-4 w-4 mr-1"/>
|
{t("howToAddRecords")}
|
||||||
{t("howToAddRecords")}
|
</Button>
|
||||||
</Button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<TableRow key={headerGroup.id} className="bg-secondary dark:bg-transparent">
|
<TableRow
|
||||||
|
key={headerGroup.id}
|
||||||
|
className="bg-secondary dark:bg-transparent"
|
||||||
|
>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<TableHead key={header.id}>
|
<TableHead key={header.id}>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
: flexRender(
|
: flexRender(
|
||||||
header.column.columnDef
|
header.column.columnDef
|
||||||
.header,
|
.header,
|
||||||
header.getContext()
|
header.getContext()
|
||||||
)}
|
)}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
))}
|
))}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -10,7 +10,16 @@ import {
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useDomainContext } from "@app/hooks/useDomainContext";
|
import { useDomainContext } from "@app/hooks/useDomainContext";
|
||||||
import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "./Settings";
|
import {
|
||||||
|
SettingsContainer,
|
||||||
|
SettingsSection,
|
||||||
|
SettingsSectionBody,
|
||||||
|
SettingsSectionDescription,
|
||||||
|
SettingsSectionFooter,
|
||||||
|
SettingsSectionForm,
|
||||||
|
SettingsSectionHeader,
|
||||||
|
SettingsSectionTitle
|
||||||
|
} from "./Settings";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -21,7 +30,13 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
FormDescription
|
FormDescription
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from "./ui/select";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
@@ -51,7 +66,6 @@ function toPunycode(domain: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function isValidDomainFormat(domain: string): boolean {
|
function isValidDomainFormat(domain: string): boolean {
|
||||||
const unicodeRegex = /^(?!:\/\/)([^\s.]+\.)*[^\s.]+$/;
|
const unicodeRegex = /^(?!:\/\/)([^\s.]+\.)*[^\s.]+$/;
|
||||||
|
|
||||||
@@ -59,9 +73,9 @@ function isValidDomainFormat(domain: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parts = domain.split('.');
|
const parts = domain.split(".");
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) {
|
if (part.length === 0 || part.startsWith("-") || part.endsWith("-")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (part.length > 63) {
|
if (part.length > 63) {
|
||||||
@@ -94,8 +108,10 @@ const certResolverOptions = [
|
|||||||
{ id: "custom", title: "Custom Resolver" }
|
{ id: "custom", title: "Custom Resolver" }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export default function DomainInfoCard({
|
||||||
export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) {
|
orgId,
|
||||||
|
domainId
|
||||||
|
}: DomainInfoCardProps) {
|
||||||
const { domain, updateDomain } = useDomainContext();
|
const { domain, updateDomain } = useDomainContext();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
@@ -111,21 +127,24 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
baseDomain: "",
|
baseDomain: "",
|
||||||
type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
|
type:
|
||||||
certResolver: domain.certResolver ?? "",
|
build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
|
||||||
|
certResolver: domain.certResolver,
|
||||||
preferWildcardCert: false
|
preferWildcardCert: false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (domain.domainId) {
|
if (domain.domainId) {
|
||||||
const certResolverValue = domain.certResolver && domain.certResolver.trim() !== ""
|
const certResolverValue =
|
||||||
? domain.certResolver
|
domain.certResolver && domain.certResolver.trim() !== ""
|
||||||
: null;
|
? domain.certResolver
|
||||||
|
: null;
|
||||||
|
|
||||||
form.reset({
|
form.reset({
|
||||||
baseDomain: domain.baseDomain || "",
|
baseDomain: domain.baseDomain || "",
|
||||||
type: (domain.type as "ns" | "cname" | "wildcard") || "wildcard",
|
type:
|
||||||
|
(domain.type as "ns" | "cname" | "wildcard") || "wildcard",
|
||||||
certResolver: certResolverValue,
|
certResolver: certResolverValue,
|
||||||
preferWildcardCert: domain.preferWildcardCert || false
|
preferWildcardCert: domain.preferWildcardCert || false
|
||||||
});
|
});
|
||||||
@@ -170,7 +189,9 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
if (!orgId || !domainId) {
|
if (!orgId || !domainId) {
|
||||||
toast({
|
toast({
|
||||||
title: t("error"),
|
title: t("error"),
|
||||||
description: t("orgOrDomainIdMissing", { fallback: "Organization or Domain ID is missing" }),
|
description: t("orgOrDomainIdMissing", {
|
||||||
|
fallback: "Organization or Domain ID is missing"
|
||||||
|
}),
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -179,7 +200,11 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
setSaveLoading(true);
|
setSaveLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.patch(
|
if (!values.certResolver) {
|
||||||
|
values.certResolver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.patch(
|
||||||
`/org/${orgId}/domain/${domainId}`,
|
`/org/${orgId}/domain/${domainId}`,
|
||||||
{
|
{
|
||||||
certResolver: values.certResolver,
|
certResolver: values.certResolver,
|
||||||
@@ -195,7 +220,9 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("success"),
|
title: t("success"),
|
||||||
description: t("domainSettingsUpdated", { fallback: "Domain settings updated successfully" }),
|
description: t("domainSettingsUpdated", {
|
||||||
|
fallback: "Domain settings updated successfully"
|
||||||
|
}),
|
||||||
variant: "default"
|
variant: "default"
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -222,30 +249,36 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<InfoSections cols={3}>
|
<InfoSections cols={3}>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>{t("type")}</InfoSectionTitle>
|
||||||
{t("type")}
|
|
||||||
</InfoSectionTitle>
|
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
<span>
|
<span>
|
||||||
{getTypeDisplay(domain.type ? domain.type : "")}
|
{getTypeDisplay(
|
||||||
|
domain.type ? domain.type : ""
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>
|
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
|
||||||
{t("status")}
|
|
||||||
</InfoSectionTitle>
|
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{domain.verified ? (
|
{domain.verified ? (
|
||||||
<Badge variant="green">{t("verified")}</Badge>
|
domain.type === "wildcard" ? (
|
||||||
|
<Badge variant="outlinePrimary">
|
||||||
|
{t("manual", {
|
||||||
|
fallback: "Manual"
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="green">
|
||||||
|
{t("verified")}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Badge variant="yellow">
|
<Badge variant="yellow">
|
||||||
{t("pending", { fallback: "Pending" })}
|
{t("pending", { fallback: "Pending" })}
|
||||||
@@ -257,20 +290,13 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{loadingRecords ? (
|
<DNSRecordsTable
|
||||||
<div className="space-y-4">
|
domainId={domain.domainId}
|
||||||
{t("loadingDNSRecords", { fallback: "Loading DNS Records..." })}
|
records={dnsRecords}
|
||||||
</div>
|
isRefreshing={isRefreshing}
|
||||||
) : (
|
type={domain.type}
|
||||||
<DNSRecordsTable
|
/>
|
||||||
domainId={domain.domainId}
|
|
||||||
records={dnsRecords}
|
|
||||||
isRefreshing={isRefreshing}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* Domain Settings - Only show for wildcard domains */}
|
|
||||||
{domain.type === "wildcard" && (
|
{domain.type === "wildcard" && (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
@@ -294,33 +320,73 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
name="certResolver"
|
name="certResolver"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{t("certResolver")}</FormLabel>
|
<FormLabel>
|
||||||
|
{t("certResolver")}
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
value={
|
value={
|
||||||
field.value === null ? "default" :
|
field.value ===
|
||||||
(field.value === "" || (field.value && field.value !== "default")) ? "custom" :
|
null
|
||||||
"default"
|
? "default"
|
||||||
|
: field.value ===
|
||||||
|
"" ||
|
||||||
|
(field.value &&
|
||||||
|
field.value !==
|
||||||
|
"default")
|
||||||
|
? "custom"
|
||||||
|
: "default"
|
||||||
}
|
}
|
||||||
onValueChange={(val) => {
|
onValueChange={(
|
||||||
if (val === "default") {
|
val
|
||||||
field.onChange(null);
|
) => {
|
||||||
} else if (val === "custom") {
|
if (
|
||||||
field.onChange("");
|
val ===
|
||||||
|
"default"
|
||||||
|
) {
|
||||||
|
field.onChange(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
} else if (
|
||||||
|
val ===
|
||||||
|
"custom"
|
||||||
|
) {
|
||||||
|
field.onChange(
|
||||||
|
""
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
field.onChange(val);
|
field.onChange(
|
||||||
|
val
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t("selectCertResolver")} />
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
"selectCertResolver"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{certResolverOptions.map((opt) => (
|
{certResolverOptions.map(
|
||||||
<SelectItem key={opt.id} value={opt.id}>
|
(
|
||||||
{opt.title}
|
opt
|
||||||
</SelectItem>
|
) => (
|
||||||
))}
|
<SelectItem
|
||||||
|
key={
|
||||||
|
opt.id
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
opt.id
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
opt.title
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -328,8 +394,10 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{form.watch("certResolver") !== null &&
|
{form.watch("certResolver") !==
|
||||||
form.watch("certResolver") !== "default" && (
|
null &&
|
||||||
|
form.watch("certResolver") !==
|
||||||
|
"default" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="certResolver"
|
name="certResolver"
|
||||||
@@ -337,9 +405,22 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("enterCustomResolver")}
|
placeholder={t(
|
||||||
value={field.value || ""}
|
"enterCustomResolver"
|
||||||
onChange={(e) => field.onChange(e.target.value)}
|
)}
|
||||||
|
value={
|
||||||
|
field.value ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
field.onChange(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -348,25 +429,39 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{form.watch("certResolver") !== null &&
|
{form.watch("certResolver") !==
|
||||||
form.watch("certResolver") !== "default" && (
|
null &&
|
||||||
|
form.watch("certResolver") !==
|
||||||
|
"default" && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="preferWildcardCert"
|
name="preferWildcardCert"
|
||||||
render={({ field: switchField }) => (
|
render={({
|
||||||
|
field: switchField
|
||||||
|
}) => (
|
||||||
<FormItem className="items-center space-y-2 mt-4">
|
<FormItem className="items-center space-y-2 mt-4">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
checked={switchField.value}
|
checked={
|
||||||
onCheckedChange={switchField.onChange}
|
switchField.value
|
||||||
|
}
|
||||||
|
onCheckedChange={
|
||||||
|
switchField.onChange
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<FormLabel>{t("preferWildcardCert")}</FormLabel>
|
<FormLabel>
|
||||||
|
{t(
|
||||||
|
"preferWildcardCert"
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
{t("preferWildcardCertDescription")}
|
{t(
|
||||||
|
"preferWildcardCertDescription"
|
||||||
|
)}
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -394,4 +489,4 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user