Various fixes for rc

This commit is contained in:
Owen
2025-10-27 16:33:21 -07:00
parent 6b18a24f9b
commit 15d63ddffa
10 changed files with 544 additions and 452 deletions

View File

@@ -1744,7 +1744,6 @@
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
"idpDisabled": "Identity providers are disabled.",
"orgAuthPageDisabled": "Organization auth page is disabled.",
"domainRestartedDescription": "Domain verification restarted successfully",
@@ -2040,5 +2039,7 @@
"version2": "Version 2",
"versionDescription": "Version 1 is text-based and widely supported. Version 2 is binary and more efficient but less compatible.",
"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"
}

View File

@@ -105,7 +105,7 @@ export async function logRequestAudit(
try {
if (data.orgId) {
const retentionDays = await getRetentionDays(data.orgId);
if (retentionDays === 0) {
if (retentionDays == 0) {
// do not log
return;
}

View File

@@ -14,7 +14,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { response } from "@server/lib/response";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { logAccessAudit } from "#private/lib/logAccessAudit";
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
const getExchangeTokenParams = z
.object({

View File

@@ -9,7 +9,7 @@ export default async function migration() {
try {
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`
CREATE TABLE "accessAuditLog" (

View File

@@ -15,7 +15,7 @@ export default async function migration() {
db.transaction(() => {
db.prepare(
`UPDATE resourceRules SET match = "COUNTRY" WHERE match = "GEOIP"`
`UPDATE 'resourceRules' SET 'match' = 'COUNTRY' WHERE 'match' = 'GEOIP'`
).run();
db.prepare(
@@ -155,7 +155,7 @@ export default async function migration() {
).run();
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();
db.prepare(`DROP TABLE 'resources';`).run();
db.prepare(

View File

@@ -12,93 +12,98 @@ import { useDomain } from "@app/contexts/domainContext";
import { useTranslations } from "next-intl";
export default function DomainSettingsPage() {
const { domain, orgId } = useDomain();
const router = useRouter();
const api = createApiClient(useEnvContext());
const [isRefreshing, setIsRefreshing] = useState(false);
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(new Set());
const t = useTranslations();
const { domain, orgId } = useDomain();
const router = useRouter();
const api = createApiClient(useEnvContext());
const [isRefreshing, setIsRefreshing] = useState(false);
const [restartingDomains, setRestartingDomains] = useState<Set<string>>(
new Set()
);
const t = useTranslations();
const { env } = useEnvContext();
const refreshData = async () => {
setIsRefreshing(true);
try {
await new Promise((resolve) => setTimeout(resolve, 200));
router.refresh();
} catch {
toast({
title: t("error"),
description: t("refreshError"),
variant: "destructive",
});
} finally {
setIsRefreshing(false);
const refreshData = async () => {
setIsRefreshing(true);
try {
await new Promise((resolve) => setTimeout(resolve, 200));
router.refresh();
} catch {
toast({
title: t("error"),
description: t("refreshError"),
variant: "destructive"
});
} finally {
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) => {
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;
});
}
};
const isRestarting = restartingDomains.has(domain.domainId);
if (!domain) {
return null;
}
const isRestarting = restartingDomains.has(domain.domainId);
return (
<>
<div className="flex justify-between">
<SettingsSectionTitle
title={domain.baseDomain}
description={t("domainSettingDescription")}
/>
<Button
variant="outline"
onClick={() => restartDomain(domain.domainId)}
disabled={isRestarting}
>
{isRestarting ? (
<>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>
{t("restarting", { fallback: "Restarting..." })}
</>
) : (
<>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>
{t("restart", { fallback: "Restart" })}
</>
)}
</Button>
</div>
<div className="space-y-6">
<DomainInfoCard orgId={orgId} domainId={domain.domainId} />
</div>
</>
);
}
return (
<>
<div className="flex justify-between">
<SettingsSectionTitle
title={domain.baseDomain}
description={t("domainSettingDescription")}
/>
{env.flags.usePangolinDns && (
<Button
variant="outline"
onClick={() => restartDomain(domain.domainId)}
disabled={isRestarting}
>
{isRestarting ? (
<>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>
{t("restarting", { fallback: "Restarting..." })}
</>
) : (
<>
<RefreshCw
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
/>
{t("restart", { fallback: "Restart" })}
</>
)}
</Button>
)}
</div>
<div className="space-y-6">
<DomainInfoCard orgId={orgId} domainId={domain.domainId} />
</div>
</>
);
}

View File

@@ -416,7 +416,7 @@ export default function GeneralPage() {
<ChevronDown className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-full">
<DropdownMenuContent>
{LOG_RETENTION_OPTIONS.filter(
(option) => {
if (
@@ -627,243 +627,256 @@ export default function GeneralPage() {
</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 */}
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("securitySettings")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("securitySettingsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SecurityFeaturesAlert />
<SettingsSectionForm>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="security-settings-form"
>
<FormField
control={form.control}
name="requireTwoFactor"
render={({ field }) => {
const isDisabled =
isSecurityFeatureDisabled();
<SettingsSectionForm>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-4"
id="security-settings-form"
>
<FormField
control={form.control}
name="requireTwoFactor"
render={({ field }) => {
const isDisabled =
isSecurityFeatureDisabled();
return (
<FormItem className="col-span-2">
<div className="flex items-center gap-2">
<FormControl>
<SwitchInput
id="require-two-factor"
defaultChecked={
field.value ||
false
}
label={t(
"requireTwoFactorForAllUsers"
)}
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 (
<FormItem className="col-span-2">
<div className="flex items-center gap-2">
return (
<FormItem className="col-span-2">
<FormLabel>
{t("maxSessionLength")}
</FormLabel>
<FormControl>
<SwitchInput
id="require-two-factor"
defaultChecked={
field.value ||
false
<Select
value={
field.value?.toString() ||
"null"
}
label={t(
"requireTwoFactorForAllUsers"
)}
disabled={
isDisabled
}
onCheckedChange={(
val
onValueChange={(
value
) => {
if (
!isDisabled
) {
const numValue =
value ===
"null"
? null
: parseInt(
value,
10
);
form.setValue(
"requireTwoFactor",
val
"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>
</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 />
{t(
"editPasswordExpiryDescription"
)}
</FormDescription>
</FormItem>
);
}}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
<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 />
{t(
"editPasswordExpiryDescription"
)}
</FormDescription>
</FormItem>
);
}}
/>
</form>
</Form>
</SettingsSectionForm>
</SettingsSectionBody>
</SettingsSection>
)}
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}

View File

@@ -18,9 +18,15 @@ type Props = {
records: DNSRecordRow[];
domainId: string;
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 columns: ColumnDef<DNSRecordRow>[] = [
@@ -28,56 +34,31 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro
accessorKey: "baseDomain",
header: ({ column }) => {
return (
<div
>
{t("recordName", { fallback: "Record name" })}
</div>
<div>{t("recordName", { fallback: "Record name" })}</div>
);
},
cell: ({ row }) => {
const baseDomain = row.original.baseDomain;
return (
<div>
{baseDomain || "-"}
</div>
);
return <div>{baseDomain || "-"}</div>;
}
},
{
accessorKey: "recordType",
header: ({ column }) => {
return (
<div
>
{t("type")}
</div>
);
return <div>{t("type")}</div>;
},
cell: ({ row }) => {
const type = row.original.recordType;
return (
<div className="">
{type}
</div>
);
return <div className="">{type}</div>;
}
},
{
accessorKey: "ttl",
header: ({ column }) => {
return (
<div
>
{t("TTL")}
</div>
);
return <div>{t("TTL")}</div>;
},
cell: ({ row }) => {
return (
<div>
{t("auto")}
</div>
);
return <div>{t("auto")}</div>;
}
},
{
@@ -87,44 +68,39 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro
},
cell: ({ row }) => {
const value = row.original.value;
return (
<div>
{value}
</div>
);
return <div>{value}</div>;
}
},
{
accessorKey: "verified",
header: ({ column }) => {
return (
<div
>
{t("status")}
</div>
);
return <div>{t("status")}</div>;
},
cell: ({ row }) => {
const verified = row.original.verified;
return (
verified ? (
<Badge variant="green">{t("verified")}</Badge>
) : (
<Badge variant="yellow">
{t("pending", { fallback: "Pending" })}
return verified ? (
type === "wildcard" ? (
<Badge variant="outlinePrimary">
{t("manual", { fallback: "Manual" })}
</Badge>
) : (
<Badge variant="green">{t("verified")}</Badge>
)
) : (
<Badge variant="yellow">
{t("pending", { fallback: "Pending" })}
</Badge>
);
}
}
];
return (
<DNSRecordsDataTable
columns={columns}
data={records}
isRefreshing={isRefreshing}
type={type}
/>
);
}
}

View File

@@ -29,7 +29,8 @@ import {
import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs";
import { useTranslations } from "next-intl";
import { Badge } from "./ui/badge";
import Link from "next/link";
import { build } from "@server/build";
type TabFilter = {
id: string;
@@ -55,6 +56,7 @@ type DNSRecordsDataTableProps<TData, TValue> = {
defaultTab?: string;
persistPageSize?: boolean | string;
defaultPageSize?: number;
type?: string | null;
};
export function DNSRecordsDataTable<TData, TValue>({
@@ -68,7 +70,7 @@ export function DNSRecordsDataTable<TData, TValue>({
defaultSort,
tabs,
defaultTab,
type
}: DNSRecordsDataTableProps<TData, TValue>) {
const t = useTranslations();
@@ -97,12 +99,9 @@ export function DNSRecordsDataTable<TData, TValue>({
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getFilteredRowModel: getFilteredRowModel()
});
return (
<div className="container mx-auto max-w-12xl">
<Card>
@@ -112,28 +111,31 @@ export function DNSRecordsDataTable<TData, TValue>({
<h1 className="font-bold">{t("dnsRecord")}</h1>
<Badge variant="secondary">{t("required")}</Badge>
</div>
<Button
variant="outline"
>
<ExternalLink className="h-4 w-4 mr-1"/>
{t("howToAddRecords")}
</Button>
<Link href="https://docs.pangolin.net/self-host/dns-and-networking">
<Button variant="outline">
<ExternalLink className="h-4 w-4 mr-1" />
{t("howToAddRecords")}
</Button>
</Link>
</div>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
{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) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef
.header,
header.getContext()
)}
header.column.columnDef
.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>

View File

@@ -10,7 +10,16 @@ import {
import { useTranslations } from "next-intl";
import { useEnvContext } from "@app/hooks/useEnvContext";
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 {
Form,
@@ -21,7 +30,13 @@ import {
FormMessage,
FormDescription
} 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 { useForm } from "react-hook-form";
import z from "zod";
@@ -51,7 +66,6 @@ function toPunycode(domain: string): string {
}
}
function isValidDomainFormat(domain: string): boolean {
const unicodeRegex = /^(?!:\/\/)([^\s.]+\.)*[^\s.]+$/;
@@ -59,9 +73,9 @@ function isValidDomainFormat(domain: string): boolean {
return false;
}
const parts = domain.split('.');
const parts = domain.split(".");
for (const part of parts) {
if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) {
if (part.length === 0 || part.startsWith("-") || part.endsWith("-")) {
return false;
}
if (part.length > 63) {
@@ -94,8 +108,10 @@ const certResolverOptions = [
{ id: "custom", title: "Custom Resolver" }
];
export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) {
export default function DomainInfoCard({
orgId,
domainId
}: DomainInfoCardProps) {
const { domain, updateDomain } = useDomainContext();
const t = useTranslations();
const { env } = useEnvContext();
@@ -111,21 +127,24 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
resolver: zodResolver(formSchema),
defaultValues: {
baseDomain: "",
type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
certResolver: domain.certResolver ?? "",
type:
build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns",
certResolver: domain.certResolver,
preferWildcardCert: false
}
});
useEffect(() => {
if (domain.domainId) {
const certResolverValue = domain.certResolver && domain.certResolver.trim() !== ""
? domain.certResolver
: null;
const certResolverValue =
domain.certResolver && domain.certResolver.trim() !== ""
? domain.certResolver
: null;
form.reset({
baseDomain: domain.baseDomain || "",
type: (domain.type as "ns" | "cname" | "wildcard") || "wildcard",
type:
(domain.type as "ns" | "cname" | "wildcard") || "wildcard",
certResolver: certResolverValue,
preferWildcardCert: domain.preferWildcardCert || false
});
@@ -170,7 +189,9 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
if (!orgId || !domainId) {
toast({
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"
});
return;
@@ -179,7 +200,11 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
setSaveLoading(true);
try {
const response = await api.patch(
if (!values.certResolver) {
values.certResolver = null;
}
await api.patch(
`/org/${orgId}/domain/${domainId}`,
{
certResolver: values.certResolver,
@@ -195,7 +220,9 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
toast({
title: t("success"),
description: t("domainSettingsUpdated", { fallback: "Domain settings updated successfully" }),
description: t("domainSettingsUpdated", {
fallback: "Domain settings updated successfully"
}),
variant: "default"
});
} catch (error) {
@@ -222,30 +249,36 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
}
};
return (
<>
<Alert>
<AlertDescription>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
{t("type")}
</InfoSectionTitle>
<InfoSectionTitle>{t("type")}</InfoSectionTitle>
<InfoSectionContent>
<span>
{getTypeDisplay(domain.type ? domain.type : "")}
{getTypeDisplay(
domain.type ? domain.type : ""
)}
</span>
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("status")}
</InfoSectionTitle>
<InfoSectionTitle>{t("status")}</InfoSectionTitle>
<InfoSectionContent>
{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">
{t("pending", { fallback: "Pending" })}
@@ -257,20 +290,13 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
</AlertDescription>
</Alert>
{loadingRecords ? (
<div className="space-y-4">
{t("loadingDNSRecords", { fallback: "Loading DNS Records..." })}
</div>
) : (
<DNSRecordsTable
domainId={domain.domainId}
records={dnsRecords}
isRefreshing={isRefreshing}
/>
)
}
<DNSRecordsTable
domainId={domain.domainId}
records={dnsRecords}
isRefreshing={isRefreshing}
type={domain.type}
/>
{/* Domain Settings - Only show for wildcard domains */}
{domain.type === "wildcard" && (
<SettingsContainer>
<SettingsSection>
@@ -294,33 +320,73 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
name="certResolver"
render={({ field }) => (
<FormItem>
<FormLabel>{t("certResolver")}</FormLabel>
<FormLabel>
{t("certResolver")}
</FormLabel>
<FormControl>
<Select
value={
field.value === null ? "default" :
(field.value === "" || (field.value && field.value !== "default")) ? "custom" :
"default"
field.value ===
null
? "default"
: field.value ===
"" ||
(field.value &&
field.value !==
"default")
? "custom"
: "default"
}
onValueChange={(val) => {
if (val === "default") {
field.onChange(null);
} else if (val === "custom") {
field.onChange("");
onValueChange={(
val
) => {
if (
val ===
"default"
) {
field.onChange(
null
);
} else if (
val ===
"custom"
) {
field.onChange(
""
);
} else {
field.onChange(val);
field.onChange(
val
);
}
}}
>
<SelectTrigger>
<SelectValue placeholder={t("selectCertResolver")} />
<SelectValue
placeholder={t(
"selectCertResolver"
)}
/>
</SelectTrigger>
<SelectContent>
{certResolverOptions.map((opt) => (
<SelectItem key={opt.id} value={opt.id}>
{opt.title}
</SelectItem>
))}
{certResolverOptions.map(
(
opt
) => (
<SelectItem
key={
opt.id
}
value={
opt.id
}
>
{
opt.title
}
</SelectItem>
)
)}
</SelectContent>
</Select>
</FormControl>
@@ -328,8 +394,10 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
</FormItem>
)}
/>
{form.watch("certResolver") !== null &&
form.watch("certResolver") !== "default" && (
{form.watch("certResolver") !==
null &&
form.watch("certResolver") !==
"default" && (
<FormField
control={form.control}
name="certResolver"
@@ -337,9 +405,22 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
<FormItem>
<FormControl>
<Input
placeholder={t("enterCustomResolver")}
value={field.value || ""}
onChange={(e) => field.onChange(e.target.value)}
placeholder={t(
"enterCustomResolver"
)}
value={
field.value ||
""
}
onChange={(
e
) =>
field.onChange(
e
.target
.value
)
}
/>
</FormControl>
<FormMessage />
@@ -348,25 +429,39 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
/>
)}
{form.watch("certResolver") !== null &&
form.watch("certResolver") !== "default" && (
{form.watch("certResolver") !==
null &&
form.watch("certResolver") !==
"default" && (
<FormField
control={form.control}
name="preferWildcardCert"
render={({ field: switchField }) => (
render={({
field: switchField
}) => (
<FormItem className="items-center space-y-2 mt-4">
<FormControl>
<div className="flex items-center space-x-2">
<Switch
checked={switchField.value}
onCheckedChange={switchField.onChange}
checked={
switchField.value
}
onCheckedChange={
switchField.onChange
}
/>
<FormLabel>{t("preferWildcardCert")}</FormLabel>
<FormLabel>
{t(
"preferWildcardCert"
)}
</FormLabel>
</div>
</FormControl>
<FormDescription>
{t("preferWildcardCertDescription")}
{t(
"preferWildcardCertDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
@@ -394,4 +489,4 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps)
)}
</>
);
}
}