From 15d63ddffa3df9eed032c112e74825152adab451 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 27 Oct 2025 16:33:21 -0700 Subject: [PATCH] Various fixes for rc --- messages/en-US.json | 5 +- server/routers/badger/logRequestAudit.ts | 2 +- server/routers/resource/getExchangeToken.ts | 2 +- server/setup/scriptsPg/1.12.0.ts | 2 +- server/setup/scriptsSqlite/1.12.0.ts | 4 +- .../settings/domains/[domainId]/page.tsx | 177 +++---- src/app/[orgId]/settings/general/page.tsx | 459 +++++++++--------- src/components/DNSRecordTable.tsx | 78 ++- src/components/DNSRecordsDataTable.tsx | 36 +- src/components/DomainInfoCard.tsx | 231 ++++++--- 10 files changed, 544 insertions(+), 452 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 2767a25cc..1b27fc03c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -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" } diff --git a/server/routers/badger/logRequestAudit.ts b/server/routers/badger/logRequestAudit.ts index b0754aed8..1a2bba022 100644 --- a/server/routers/badger/logRequestAudit.ts +++ b/server/routers/badger/logRequestAudit.ts @@ -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; } diff --git a/server/routers/resource/getExchangeToken.ts b/server/routers/resource/getExchangeToken.ts index 1ed461c9e..289752340 100644 --- a/server/routers/resource/getExchangeToken.ts +++ b/server/routers/resource/getExchangeToken.ts @@ -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({ diff --git a/server/setup/scriptsPg/1.12.0.ts b/server/setup/scriptsPg/1.12.0.ts index 0c710127f..98487cd8e 100644 --- a/server/setup/scriptsPg/1.12.0.ts +++ b/server/setup/scriptsPg/1.12.0.ts @@ -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" ( diff --git a/server/setup/scriptsSqlite/1.12.0.ts b/server/setup/scriptsSqlite/1.12.0.ts index 0d1d98625..393abdb6a 100644 --- a/server/setup/scriptsSqlite/1.12.0.ts +++ b/server/setup/scriptsSqlite/1.12.0.ts @@ -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( diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx index c7e137f68..d3c6da369 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/page.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx @@ -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>(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>( + 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 ( - <> -
- - -
-
- -
- - ); -} \ No newline at end of file + return ( + <> +
+ + {env.flags.usePangolinDns && ( + + )} +
+
+ +
+ + ); +} diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 8f2e68207..72d1ff080 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -416,7 +416,7 @@ export default function GeneralPage() { - + {LOG_RETENTION_OPTIONS.filter( (option) => { if ( @@ -627,243 +627,256 @@ export default function GeneralPage() { - {build === "saas" && } + {build !== "oss" && ( + + + + {t("securitySettings")} + + + {t("securitySettingsDescription")} + + + + - {/* Security Settings Section */} - - - - {t("securitySettings")} - - - {t("securitySettingsDescription")} - - - - + +
+ + { + const isDisabled = + isSecurityFeatureDisabled(); - - - - { - const isDisabled = - isSecurityFeatureDisabled(); + return ( + +
+ + { + if ( + !isDisabled + ) { + form.setValue( + "requireTwoFactor", + val + ); + } + }} + /> + +
+ + + {t( + "requireTwoFactorDescription" + )} + +
+ ); + }} + /> + { + const isDisabled = + isSecurityFeatureDisabled(); - return ( - -
+ return ( + + + {t("maxSessionLength")} + - { if ( !isDisabled ) { + const numValue = + value === + "null" + ? null + : parseInt( + value, + 10 + ); form.setValue( - "requireTwoFactor", - val + "maxSessionLengthHours", + numValue ); } }} - /> + disabled={ + isDisabled + } + > + + + + + {SESSION_LENGTH_OPTIONS.map( + ( + option + ) => ( + + {t( + option.labelKey + )} + + ) + )} + + -
- - - {t( - "requireTwoFactorDescription" - )} - -
- ); - }} - /> - { - const isDisabled = - isSecurityFeatureDisabled(); - - return ( - - - {t("maxSessionLength")} - - - - - - - {t( - "maxSessionLengthDescription" - )} - - - ); - }} - /> - { - const isDisabled = - isSecurityFeatureDisabled(); - - return ( - - - {t("passwordExpiryDays")} - - - - - - {t( - "editPasswordExpiryDescription" - )} - - - ); - }} - /> - - -
-
-
+ + {t( + "maxSessionLengthDescription" + )} + + + ); + }} + /> + { + const isDisabled = + isSecurityFeatureDisabled(); + + return ( + + + {t( + "passwordExpiryDays" + )} + + + + + + + {t( + "editPasswordExpiryDescription" + )} + + + ); + }} + /> + + + +
+
+ )} {build === "saas" && } diff --git a/src/components/DNSRecordTable.tsx b/src/components/DNSRecordTable.tsx index 8d8e4024b..2566eb712 100644 --- a/src/components/DNSRecordTable.tsx +++ b/src/components/DNSRecordTable.tsx @@ -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[] = [ @@ -28,56 +34,31 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro accessorKey: "baseDomain", header: ({ column }) => { return ( -
- {t("recordName", { fallback: "Record name" })} -
+
{t("recordName", { fallback: "Record name" })}
); }, cell: ({ row }) => { const baseDomain = row.original.baseDomain; - return ( -
- {baseDomain || "-"} -
- ); + return
{baseDomain || "-"}
; } }, { accessorKey: "recordType", header: ({ column }) => { - return ( -
- {t("type")} -
- ); + return
{t("type")}
; }, cell: ({ row }) => { const type = row.original.recordType; - return ( -
- {type} -
- ); + return
{type}
; } }, { accessorKey: "ttl", header: ({ column }) => { - return ( -
- {t("TTL")} -
- ); + return
{t("TTL")}
; }, cell: ({ row }) => { - return ( -
- {t("auto")} -
- ); + return
{t("auto")}
; } }, { @@ -87,44 +68,39 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro }, cell: ({ row }) => { const value = row.original.value; - return ( -
- {value} -
- ); + return
{value}
; } }, { accessorKey: "verified", header: ({ column }) => { - return ( -
- {t("status")} -
- ); + return
{t("status")}
; }, cell: ({ row }) => { const verified = row.original.verified; - return ( - verified ? ( - {t("verified")} - ) : ( - - {t("pending", { fallback: "Pending" })} + return verified ? ( + type === "wildcard" ? ( + + {t("manual", { fallback: "Manual" })} + ) : ( + {t("verified")} ) + ) : ( + + {t("pending", { fallback: "Pending" })} + ); } } ]; - return ( ); -} \ No newline at end of file +} diff --git a/src/components/DNSRecordsDataTable.tsx b/src/components/DNSRecordsDataTable.tsx index 5d179f302..0fe0d2765 100644 --- a/src/components/DNSRecordsDataTable.tsx +++ b/src/components/DNSRecordsDataTable.tsx @@ -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 = { defaultTab?: string; persistPageSize?: boolean | string; defaultPageSize?: number; + type?: string | null; }; export function DNSRecordsDataTable({ @@ -68,7 +70,7 @@ export function DNSRecordsDataTable({ defaultSort, tabs, defaultTab, - + type }: DNSRecordsDataTableProps) { const t = useTranslations(); @@ -97,12 +99,9 @@ export function DNSRecordsDataTable({ getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), + getFilteredRowModel: getFilteredRowModel() }); - - - return (
@@ -112,28 +111,31 @@ export function DNSRecordsDataTable({

{t("dnsRecord")}

{t("required")}
- + + + {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => ( {header.isPlaceholder ? null : flexRender( - header.column.columnDef - .header, - header.getContext() - )} + header.column.columnDef + .header, + header.getContext() + )} ))} diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx index 15fe5ce07..0d0da84b0 100644 --- a/src/components/DomainInfoCard.tsx +++ b/src/components/DomainInfoCard.tsx @@ -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 ( <> - - {t("type")} - + {t("type")} - {getTypeDisplay(domain.type ? domain.type : "")} + {getTypeDisplay( + domain.type ? domain.type : "" + )} - - {t("status")} - + {t("status")} {domain.verified ? ( - {t("verified")} + domain.type === "wildcard" ? ( + + {t("manual", { + fallback: "Manual" + })} + + ) : ( + + {t("verified")} + + ) ) : ( {t("pending", { fallback: "Pending" })} @@ -257,20 +290,13 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) - {loadingRecords ? ( -
- {t("loadingDNSRecords", { fallback: "Loading DNS Records..." })} -
- ) : ( - - ) - } + - {/* Domain Settings - Only show for wildcard domains */} {domain.type === "wildcard" && ( @@ -294,33 +320,73 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) name="certResolver" render={({ field }) => ( - {t("certResolver")} + + {t("certResolver")} + @@ -328,8 +394,10 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) )} /> - {form.watch("certResolver") !== null && - form.watch("certResolver") !== "default" && ( + {form.watch("certResolver") !== + null && + form.watch("certResolver") !== + "default" && ( field.onChange(e.target.value)} + placeholder={t( + "enterCustomResolver" + )} + value={ + field.value || + "" + } + onChange={( + e + ) => + field.onChange( + e + .target + .value + ) + } /> @@ -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" && ( ( + render={({ + field: switchField + }) => (
- {t("preferWildcardCert")} + + {t( + "preferWildcardCert" + )} +
- {t("preferWildcardCertDescription")} + {t( + "preferWildcardCertDescription" + )}
@@ -394,4 +489,4 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) )} ); -} \ No newline at end of file +}