diff --git a/messages/en-US.json b/messages/en-US.json index 2767a25c..1b27fc03 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 b0754aed..1a2bba02 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 1ed461c9..28975234 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 0c710127..98487cd8 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 0d1d9862..393abdb6 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 c7e137f6..d3c6da36 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 8f2e6820..72d1ff08 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 8d8e4024..2566eb71 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 5d179f30..0fe0d276 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 15fe5ce0..0d0da84b 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 +}