From 3f3e9cf1bb8fd280482db6e68661f516e6650858 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Mon, 6 Oct 2025 21:00:07 +0530 Subject: [PATCH 01/38] add cert resolver --- messages/en-US.json | 6 +- server/db/sqlite/schema/schema.ts | 4 +- server/routers/domain/createOrgDomain.ts | 17 +++-- server/routers/domain/listDomains.ts | 4 +- src/components/CreateDomainForm.tsx | 83 +++++++++++++++++++----- 5 files changed, 92 insertions(+), 22 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 4cffaf98..2f7fd3e0 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1891,5 +1891,9 @@ "cannotbeUndone": "This can not be undone.", "toConfirm": "to confirm", "deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?", - "clientMessageRemove": "Once removed, the client will no longer be able to connect to the site." + "clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.", + "certResolver": "Certificate Resolver", + "certResolverDescription": "Select the certificate resolver to use for this resource.", + "selectCertResolver": "Select Certificate Resolver", + "enterCustomResolver": "Enter Custom Resolver" } diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 3d6c6b0d..d0d972ff 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -11,7 +11,9 @@ export const domains = sqliteTable("domains", { type: text("type"), // "ns", "cname", "wildcard" verified: integer("verified", { mode: "boolean" }).notNull().default(false), failed: integer("failed", { mode: "boolean" }).notNull().default(false), - tries: integer("tries").notNull().default(0) + tries: integer("tries").notNull().default(0), + certResolver: text("certResolver").default("letsencrypt"), + customCertResolver: text("customCertResolver") }); export const orgs = sqliteTable("orgs", { diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index d0e8a72b..54cad172 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -24,16 +24,21 @@ const paramsSchema = z const bodySchema = z .object({ type: z.enum(["ns", "cname", "wildcard"]), - baseDomain: subdomainSchema + baseDomain: subdomainSchema, + certResolver: z.enum(["letsencrypt", "custom"]).optional(), // optional, only for wildcard + customCertResolver: z.string().optional() // required if certResolver === "custom" }) .strict(); + export type CreateDomainResponse = { domainId: string; nsRecords?: string[]; cnameRecords?: { baseDomain: string; value: string }[]; aRecords?: { baseDomain: string; value: string }[]; txtRecords?: { baseDomain: string; value: string }[]; + certResolver?: string | null; + customCertResolver?: string | null; }; // Helper to check if a domain is a subdomain or equal to another domain @@ -71,7 +76,7 @@ export async function createOrgDomain( } const { orgId } = parsedParams.data; - const { type, baseDomain } = parsedBody.data; + const { type, baseDomain, certResolver, customCertResolver } = parsedBody.data; if (build == "oss") { if (type !== "wildcard") { @@ -254,7 +259,9 @@ export async function createOrgDomain( domainId, baseDomain, type, - verified: type === "wildcard" ? true : false + verified: type === "wildcard" ? true : false, + certResolver: certResolver || null, + customCertResolver: customCertResolver || null }) .returning(); @@ -325,7 +332,9 @@ export async function createOrgDomain( cnameRecords, txtRecords, nsRecords, - aRecords + aRecords, + certResolver: returned.certResolver, + customCertResolver: returned.customCertResolver }, success: true, error: false, diff --git a/server/routers/domain/listDomains.ts b/server/routers/domain/listDomains.ts index fe51cde6..85bc3caa 100644 --- a/server/routers/domain/listDomains.ts +++ b/server/routers/domain/listDomains.ts @@ -42,7 +42,9 @@ async function queryDomains(orgId: string, limit: number, offset: number) { type: domains.type, failed: domains.failed, tries: domains.tries, - configManaged: domains.configManaged + configManaged: domains.configManaged, + certResolver: domains.certResolver, + customCertResolver: domains.customCertResolver, }) .from(orgDomains) .where(eq(orgDomains.orgId, orgId)) diff --git a/src/components/CreateDomainForm.tsx b/src/components/CreateDomainForm.tsx index 258aee49..24b1d466 100644 --- a/src/components/CreateDomainForm.tsx +++ b/src/components/CreateDomainForm.tsx @@ -45,6 +45,7 @@ import { import { useOrgContext } from "@app/hooks/useOrgContext"; import { build } from "@server/build"; import { toASCII, toUnicode } from 'punycode'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; // Helper functions for Unicode domain handling @@ -96,7 +97,9 @@ const formSchema = z.object({ .min(1, "Domain is required") .refine((val) => isValidDomainFormat(val), "Invalid domain format") .transform((val) => toPunycode(val)), - type: z.enum(["ns", "cname", "wildcard"]) + type: z.enum(["ns", "cname", "wildcard"]), + certResolver: z.string().optional(), + customCertResolver: z.string().optional() }); type FormValues = z.infer; @@ -107,6 +110,12 @@ type CreateDomainFormProps = { onCreated?: (domain: CreateDomainResponse) => void; }; +// Example cert resolver options (replace with real API/fetch if needed) +const certResolverOptions = [ + { id: "letsencrypt", title: "Let's Encrypt" }, + { id: "custom", title: "Custom Resolver" } +]; + export default function CreateDomainForm({ open, setOpen, @@ -125,15 +134,26 @@ export default function CreateDomainForm({ resolver: zodResolver(formSchema), defaultValues: { baseDomain: "", - type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns" + type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns", + certResolver: "", + customCertResolver: "" } }); - function reset() { + const baseDomain = form.watch("baseDomain"); + const domainType = form.watch("type"); + + const punycodePreview = useMemo(() => { + if (!baseDomain) return ""; + const punycode = toPunycode(baseDomain); + return punycode !== baseDomain.toLowerCase() ? punycode : ""; + }, [baseDomain]); + + const reset = () => { form.reset(); setLoading(false); setCreatedDomain(null); - } + }; async function onSubmit(values: FormValues) { setLoading(true); @@ -158,17 +178,9 @@ export default function CreateDomainForm({ } finally { setLoading(false); } - } - - const baseDomain = form.watch("baseDomain"); - const domainInputValue = form.watch("baseDomain") || ""; - - const punycodePreview = useMemo(() => { - if (!domainInputValue) return ""; - const punycode = toPunycode(domainInputValue); - return punycode !== domainInputValue.toLowerCase() ? punycode : ""; - }, [domainInputValue]); + }; + // Domain type options let domainOptions: any = []; if (build != "oss" && env.flags.usePangolinDns) { domainOptions = [ @@ -250,7 +262,7 @@ export default function CreateDomainForm({ {t("internationaldomaindetected")}
-

{t("willbestoredas")} {punycodePreview}

+

{t("willbestoredas")} {punycodePreview}

@@ -260,6 +272,47 @@ export default function CreateDomainForm({ )} /> + {domainType === "wildcard" && ( + ( + + {t("certResolver")} + + + + + {field.value === "custom" && ( + + + form.setValue("customCertResolver", e.target.value) + } + /> + + )} + + )} + /> + + )} ) : ( From d30e0a3c514434d537cf3caa1271826a7439ba44 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Mon, 6 Oct 2025 21:39:00 +0530 Subject: [PATCH 02/38] schema add --- server/db/pg/schema/schema.ts | 4 +++- src/components/DomainsTable.tsx | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 4bed23f8..01fdd337 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -18,7 +18,9 @@ export const domains = pgTable("domains", { type: varchar("type"), // "ns", "cname", "wildcard" verified: boolean("verified").notNull().default(false), failed: boolean("failed").notNull().default(false), - tries: integer("tries").notNull().default(0) + tries: integer("tries").notNull().default(0), + certResolver: varchar("certResolver").default("letsencrypt"), + customCertResolver: varchar("customCertResolver") }); export const orgs = pgTable("orgs", { diff --git a/src/components/DomainsTable.tsx b/src/components/DomainsTable.tsx index ca8d2a7c..87d869df 100644 --- a/src/components/DomainsTable.tsx +++ b/src/components/DomainsTable.tsx @@ -24,6 +24,8 @@ export type DomainRow = { failed: boolean; tries: number; configManaged: boolean; + certResolver: string; + customCertResolver: string; }; type Props = { From 2f1aec02f0f7678a59f7179bd49830dadd3672f5 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Tue, 7 Oct 2025 00:37:54 +0530 Subject: [PATCH 03/38] traefik config update for custom Cert Resolver --- server/db/pg/schema/schema.ts | 2 +- server/db/sqlite/schema/schema.ts | 2 +- server/lib/readConfigFile.ts | 2 + server/lib/traefik/getTraefikConfig.ts | 84 ++++++++++++++++++++++---- 4 files changed, 77 insertions(+), 13 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 01fdd337..33b2ae28 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -19,7 +19,7 @@ export const domains = pgTable("domains", { verified: boolean("verified").notNull().default(false), failed: boolean("failed").notNull().default(false), tries: integer("tries").notNull().default(0), - certResolver: varchar("certResolver").default("letsencrypt"), + certResolver: varchar("certResolver"), customCertResolver: varchar("customCertResolver") }); diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index d0d972ff..ebfe7bcf 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -12,7 +12,7 @@ export const domains = sqliteTable("domains", { verified: integer("verified", { mode: "boolean" }).notNull().default(false), failed: integer("failed", { mode: "boolean" }).notNull().default(false), tries: integer("tries").notNull().default(0), - certResolver: text("certResolver").default("letsencrypt"), + certResolver: text("certResolver"), customCertResolver: text("customCertResolver") }); diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 9aee8531..a1899f4e 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -51,6 +51,7 @@ export const configSchema = z .nonempty("base_domain must not be empty") .transform((url) => url.toLowerCase()), cert_resolver: z.string().optional().default("letsencrypt"), + custom_cert_resolver: z.string().optional(), prefer_wildcard_cert: z.boolean().optional().default(false) }) ) @@ -187,6 +188,7 @@ export const configSchema = z https_entrypoint: z.string().optional().default("websecure"), additional_middlewares: z.array(z.string()).optional(), cert_resolver: z.string().optional().default("letsencrypt"), + custom_cert_resolver: z.string().optional(), prefer_wildcard_cert: z.boolean().optional().default(false), certificates_path: z.string().default("/var/certificates"), monitor_interval: z.number().default(5000), diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index 75ea907f..734327e2 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -1,4 +1,4 @@ -import { db, targetHealthCheck } from "@server/db"; +import { db, targetHealthCheck, domains } from "@server/db"; import { and, eq, @@ -75,11 +75,15 @@ export async function getTraefikConfig( siteType: sites.type, siteOnline: sites.online, subnet: sites.subnet, - exitNodeId: sites.exitNodeId + exitNodeId: sites.exitNodeId, + // Domain cert resolver fields + domainCertResolver: domains.certResolver, + domainCustomCertResolver: domains.customCertResolver }) .from(sites) .innerJoin(targets, eq(targets.siteId, sites.siteId)) .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) + .leftJoin(domains, eq(domains.domainId, resources.domainId)) .leftJoin( targetHealthCheck, eq(targetHealthCheck.targetId, targets.targetId) @@ -161,7 +165,10 @@ export async function getTraefikConfig( pathMatchType: row.pathMatchType, // the targets will all have the same pathMatchType rewritePath: row.rewritePath, rewritePathType: row.rewritePathType, - priority: priority // may be null, we fallback later + priority: priority, + // Store domain cert resolver fields + domainCertResolver: row.domainCertResolver, + domainCustomCertResolver: row.domainCustomCertResolver }); } @@ -242,18 +249,73 @@ export async function getTraefikConfig( const configDomain = config.getDomain(resource.domainId); - let certResolver: string, preferWildcardCert: boolean; - if (!configDomain) { - certResolver = config.getRawConfig().traefik.cert_resolver; - preferWildcardCert = - config.getRawConfig().traefik.prefer_wildcard_cert; + let certResolverFromConfig: string | undefined; + let preferWildcardCert = false; + + const rawTraefikCfg = config.getRawConfig().traefik || {}; + const globalDefaultResolver: string | undefined = rawTraefikCfg.cert_resolver; + const availableResolvers = rawTraefikCfg.custom_cert_resolver + ? Object.keys(rawTraefikCfg.custom_cert_resolver) + : []; + + // Priority 1: Read from YAML config (if exists) + if (configDomain) { + certResolverFromConfig = + configDomain.cert_resolver ?? + configDomain.custom_cert_resolver; + preferWildcardCert = !!(configDomain.prefer_wildcard_cert); + } + + // Priority 2: Override with database domain settings (if exists) + let finalCertResolver: string | undefined; + let finalCustomCertResolver: string | undefined; + + if (resource.domainCertResolver) { + finalCertResolver = resource.domainCertResolver; + if (resource.domainCertResolver === "custom" && resource.domainCustomCertResolver) { + finalCustomCertResolver = resource.domainCustomCertResolver; + } } else { - certResolver = configDomain.cert_resolver; - preferWildcardCert = configDomain.prefer_wildcard_cert; + // Fall back to config + finalCertResolver = certResolverFromConfig; + } + + // Resolve the final resolver name + let resolverName: string | undefined; + + if (finalCertResolver) { + if (finalCertResolver === "custom") { + // Check database custom resolver first, then config + const customResolver = finalCustomCertResolver || configDomain?.custom_cert_resolver; + + if (customResolver && typeof customResolver === "string" && customResolver.trim()) { + resolverName = customResolver.trim(); + } else { + resolverName = globalDefaultResolver; + logger.warn( + `Domain ${resource.domainId} requested custom cert resolver but none set; falling back to global resolver ${resolverName}` + ); + } + } else { + // Validate against available resolvers + if ( + availableResolvers.length === 0 || + availableResolvers.includes(finalCertResolver) + ) { + resolverName = finalCertResolver; + } else { + logger.warn( + `Unknown cert resolver "${finalCertResolver}" for domain ${resource.domainId}; falling back to global resolver.` + ); + resolverName = globalDefaultResolver; + } + } + } else { + resolverName = globalDefaultResolver; } const tls = { - certResolver: certResolver, + certResolver: resolverName, ...(preferWildcardCert ? { domains: [ From d6681733ddd1515a9bc70099daacd2d3960a33a4 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Tue, 7 Oct 2025 12:28:29 +0530 Subject: [PATCH 04/38] remove custom cery type form config file --- server/lib/readConfigFile.ts | 2 - server/lib/traefik/getTraefikConfig.ts | 69 +++---------- .../private/lib/traefik/getTraefikConfig.ts | 99 +++++++++++++------ 3 files changed, 84 insertions(+), 86 deletions(-) diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index a1899f4e..9aee8531 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -51,7 +51,6 @@ export const configSchema = z .nonempty("base_domain must not be empty") .transform((url) => url.toLowerCase()), cert_resolver: z.string().optional().default("letsencrypt"), - custom_cert_resolver: z.string().optional(), prefer_wildcard_cert: z.boolean().optional().default(false) }) ) @@ -188,7 +187,6 @@ export const configSchema = z https_entrypoint: z.string().optional().default("websecure"), additional_middlewares: z.array(z.string()).optional(), cert_resolver: z.string().optional().default("letsencrypt"), - custom_cert_resolver: z.string().optional(), prefer_wildcard_cert: z.boolean().optional().default(false), certificates_path: z.string().default("/var/certificates"), monitor_interval: z.number().default(5000), diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index 734327e2..436c76a6 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -248,68 +248,24 @@ export async function getTraefikConfig( } const configDomain = config.getDomain(resource.domainId); - - let certResolverFromConfig: string | undefined; - let preferWildcardCert = false; - const rawTraefikCfg = config.getRawConfig().traefik || {}; - const globalDefaultResolver: string | undefined = rawTraefikCfg.cert_resolver; - const availableResolvers = rawTraefikCfg.custom_cert_resolver - ? Object.keys(rawTraefikCfg.custom_cert_resolver) - : []; + const globalDefaultResolver = rawTraefikCfg.cert_resolver; - // Priority 1: Read from YAML config (if exists) - if (configDomain) { - certResolverFromConfig = - configDomain.cert_resolver ?? - configDomain.custom_cert_resolver; - preferWildcardCert = !!(configDomain.prefer_wildcard_cert); - } - // Priority 2: Override with database domain settings (if exists) - let finalCertResolver: string | undefined; - let finalCustomCertResolver: string | undefined; + const domainCertResolver = + resource.domainCertResolver ?? configDomain?.cert_resolver; + const domainCustomResolver = + resource.domainCustomCertResolver; + const preferWildcardCert = + resource.preferWildcardCert ?? configDomain?.prefer_wildcard_cert ?? false; - if (resource.domainCertResolver) { - finalCertResolver = resource.domainCertResolver; - if (resource.domainCertResolver === "custom" && resource.domainCustomCertResolver) { - finalCustomCertResolver = resource.domainCustomCertResolver; - } - } else { - // Fall back to config - finalCertResolver = certResolverFromConfig; - } - - // Resolve the final resolver name let resolverName: string | undefined; - if (finalCertResolver) { - if (finalCertResolver === "custom") { - // Check database custom resolver first, then config - const customResolver = finalCustomCertResolver || configDomain?.custom_cert_resolver; - - if (customResolver && typeof customResolver === "string" && customResolver.trim()) { - resolverName = customResolver.trim(); - } else { - resolverName = globalDefaultResolver; - logger.warn( - `Domain ${resource.domainId} requested custom cert resolver but none set; falling back to global resolver ${resolverName}` - ); - } - } else { - // Validate against available resolvers - if ( - availableResolvers.length === 0 || - availableResolvers.includes(finalCertResolver) - ) { - resolverName = finalCertResolver; - } else { - logger.warn( - `Unknown cert resolver "${finalCertResolver}" for domain ${resource.domainId}; falling back to global resolver.` - ); - resolverName = globalDefaultResolver; - } - } + // Handle both letsencrypt & custom cases + if (domainCertResolver === "custom") { + resolverName = domainCustomResolver?.trim(); + } else if (domainCertResolver) { + resolverName = domainCertResolver; } else { resolverName = globalDefaultResolver; } @@ -327,6 +283,7 @@ export async function getTraefikConfig( : {}) }; + const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || []; diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 5e919fda..634bc818 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -15,6 +15,7 @@ import { certificates, db, domainNamespaces, + domains, exitNodes, loginPage, targetHealthCheck @@ -103,11 +104,17 @@ export async function getTraefikConfig( subnet: sites.subnet, exitNodeId: sites.exitNodeId, // Namespace - domainNamespaceId: domainNamespaces.domainNamespaceId + domainNamespaceId: domainNamespaces.domainNamespaceId, + // Certificate + certificateStatus: certificates.status, + domainCertResolver: domains.certResolver, + domainCustomCertResolver: domains.customCertResolver }) .from(sites) .innerJoin(targets, eq(targets.siteId, sites.siteId)) .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) + .leftJoin(certificates, eq(certificates.domainId, resources.domainId)) + .leftJoin(domains, eq(domains.domainId, resources.domainId)) .leftJoin( targetHealthCheck, eq(targetHealthCheck.targetId, targets.targetId) @@ -197,7 +204,9 @@ export async function getTraefikConfig( pathMatchType: row.pathMatchType, // the targets will all have the same pathMatchType rewritePath: row.rewritePath, rewritePathType: row.rewritePathType, - priority: priority // may be null, we fallback later + priority: priority, // may be null, we fallback later + domainCertResolver: row.domainCertResolver, + domainCustomCertResolver: row.domainCustomCertResolver }); } @@ -285,6 +294,41 @@ export async function getTraefikConfig( config_output.http.services = {}; } + const domainParts = fullDomain.split("."); + let wildCard; + if (domainParts.length <= 2) { + wildCard = `*.${domainParts.join(".")}`; + } else { + wildCard = `*.${domainParts.slice(1).join(".")}`; + } + + if (!resource.subdomain) { + wildCard = resource.fullDomain; + } + + const configDomain = config.getDomain(resource.domainId); + const rawTraefikCfg = config.getRawConfig().traefik || {}; + const globalDefaultResolver = rawTraefikCfg.cert_resolver; + + + const domainCertResolver = + resource.domainCertResolver ?? configDomain?.cert_resolver; + const domainCustomResolver = + resource.domainCustomCertResolver; + const preferWildcardCert = + resource.preferWildcardCert ?? configDomain?.prefer_wildcard_cert ?? false; + + let resolverName: string | undefined; + + // Handle both letsencrypt & custom cases + if (domainCertResolver === "custom") { + resolverName = domainCustomResolver?.trim(); + } else if (domainCertResolver) { + resolverName = domainCertResolver; + } else { + resolverName = globalDefaultResolver; + } + let tls = {}; if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { const domainParts = fullDomain.split("."); @@ -312,16 +356,16 @@ export async function getTraefikConfig( } tls = { - certResolver: certResolver, + certResolver: resolverName, ...(preferWildcardCert ? { - domains: [ - { - main: wildCard - } - ] - } - : {}) + domains: [ + { + main: wildCard, + }, + ], + } + : {}), }; } else { // find a cert that matches the full domain, if not continue @@ -573,14 +617,14 @@ export async function getTraefikConfig( })(), ...(resource.stickySession ? { - sticky: { - cookie: { - name: "p_sticky", // TODO: make this configurable via config.yml like other cookies - secure: resource.ssl, - httpOnly: true - } - } - } + sticky: { + cookie: { + name: "p_sticky", // TODO: make this configurable via config.yml like other cookies + secure: resource.ssl, + httpOnly: true + } + } + } : {}) } }; @@ -681,13 +725,13 @@ export async function getTraefikConfig( })(), ...(resource.stickySession ? { - sticky: { - ipStrategy: { - depth: 0, - sourcePort: true - } - } - } + sticky: { + ipStrategy: { + depth: 0, + sourcePort: true + } + } + } : {}) } }; @@ -735,10 +779,9 @@ export async function getTraefikConfig( loadBalancer: { servers: [ { - url: `http://${ - config.getRawConfig().server + url: `http://${config.getRawConfig().server .internal_hostname - }:${config.getRawConfig().server.next_port}` + }:${config.getRawConfig().server.next_port}` } ] } From d938345debe8a515a8d251cdf4bd448e75ef811e Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 8 Oct 2025 14:43:24 -0700 Subject: [PATCH 05/38] Copy in config to db, remove 2nd column, + prefer --- messages/en-US.json | 3 +- server/db/pg/schema/schema.ts | 3 +- server/db/sqlite/schema/schema.ts | 2 +- server/lib/traefik/getTraefikConfig.ts | 65 +++++++++--------- .../private/lib/traefik/getTraefikConfig.ts | 25 ------- server/routers/domain/createOrgDomain.ts | 12 ++-- server/routers/domain/listDomains.ts | 2 +- server/setup/copyInConfig.ts | 12 ++-- src/components/CreateDomainForm.tsx | 66 ++++++++++++++----- 9 files changed, 103 insertions(+), 87 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 2f7fd3e0..c3e93ba8 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1895,5 +1895,6 @@ "certResolver": "Certificate Resolver", "certResolverDescription": "Select the certificate resolver to use for this resource.", "selectCertResolver": "Select Certificate Resolver", - "enterCustomResolver": "Enter Custom Resolver" + "enterCustomResolver": "Enter Custom Resolver", + "preferWildcardCert": "Prefer Wildcard Certificate" } diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 33b2ae28..ae5205bb 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -20,7 +20,8 @@ export const domains = pgTable("domains", { failed: boolean("failed").notNull().default(false), tries: integer("tries").notNull().default(0), certResolver: varchar("certResolver"), - customCertResolver: varchar("customCertResolver") + customCertResolver: varchar("customCertResolver"), + preferWildcardCert: boolean("preferWildcardCert") }); export const orgs = pgTable("orgs", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index ebfe7bcf..30841a4b 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -13,7 +13,7 @@ export const domains = sqliteTable("domains", { failed: integer("failed", { mode: "boolean" }).notNull().default(false), tries: integer("tries").notNull().default(0), certResolver: text("certResolver"), - customCertResolver: text("customCertResolver") + preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }) }); export const orgs = sqliteTable("orgs", { diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index 436c76a6..45729a30 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -77,8 +77,7 @@ export async function getTraefikConfig( subnet: sites.subnet, exitNodeId: sites.exitNodeId, // Domain cert resolver fields - domainCertResolver: domains.certResolver, - domainCustomCertResolver: domains.customCertResolver + domainCertResolver: domains.certResolver }) .from(sites) .innerJoin(targets, eq(targets.siteId, sites.siteId)) @@ -167,8 +166,7 @@ export async function getTraefikConfig( rewritePathType: row.rewritePathType, priority: priority, // Store domain cert resolver fields - domainCertResolver: row.domainCertResolver, - domainCustomCertResolver: row.domainCustomCertResolver + domainCertResolver: row.domainCertResolver }); } @@ -247,42 +245,47 @@ export async function getTraefikConfig( wildCard = resource.fullDomain; } - const configDomain = config.getDomain(resource.domainId); - const rawTraefikCfg = config.getRawConfig().traefik || {}; - const globalDefaultResolver = rawTraefikCfg.cert_resolver; + const globalDefaultResolver = + config.getRawConfig().traefik.cert_resolver; + const globalDefaultPreferWildcard = + config.getRawConfig().traefik.prefer_wildcard_cert; - - const domainCertResolver = - resource.domainCertResolver ?? configDomain?.cert_resolver; - const domainCustomResolver = - resource.domainCustomCertResolver; - const preferWildcardCert = - resource.preferWildcardCert ?? configDomain?.prefer_wildcard_cert ?? false; + const domainCertResolver = resource.domainCertResolver; + const preferWildcardCert = resource.preferWildcardCert; let resolverName: string | undefined; - + let preferWildcard: boolean | undefined; // Handle both letsencrypt & custom cases - if (domainCertResolver === "custom") { - resolverName = domainCustomResolver?.trim(); - } else if (domainCertResolver) { - resolverName = domainCertResolver; + if (domainCertResolver) { + resolverName = domainCertResolver.trim(); } else { resolverName = globalDefaultResolver; } - const tls = { - certResolver: resolverName, - ...(preferWildcardCert - ? { - domains: [ - { - main: wildCard - } - ] - } - : {}) - }; + if ( + preferWildcardCert !== undefined && + preferWildcardCert !== null + ) { + preferWildcard = preferWildcardCert; + } else { + preferWildcard = globalDefaultPreferWildcard; + } + let tls = {}; + if (build == "oss") { + tls = { + certResolver: resolverName, + ...(preferWildcard + ? { + domains: [ + { + main: wildCard + } + ] + } + : {}) + }; + } const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || []; diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 634bc818..c0f934cd 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -108,7 +108,6 @@ export async function getTraefikConfig( // Certificate certificateStatus: certificates.status, domainCertResolver: domains.certResolver, - domainCustomCertResolver: domains.customCertResolver }) .from(sites) .innerJoin(targets, eq(targets.siteId, sites.siteId)) @@ -206,7 +205,6 @@ export async function getTraefikConfig( rewritePathType: row.rewritePathType, priority: priority, // may be null, we fallback later domainCertResolver: row.domainCertResolver, - domainCustomCertResolver: row.domainCustomCertResolver }); } @@ -306,29 +304,6 @@ export async function getTraefikConfig( wildCard = resource.fullDomain; } - const configDomain = config.getDomain(resource.domainId); - const rawTraefikCfg = config.getRawConfig().traefik || {}; - const globalDefaultResolver = rawTraefikCfg.cert_resolver; - - - const domainCertResolver = - resource.domainCertResolver ?? configDomain?.cert_resolver; - const domainCustomResolver = - resource.domainCustomCertResolver; - const preferWildcardCert = - resource.preferWildcardCert ?? configDomain?.prefer_wildcard_cert ?? false; - - let resolverName: string | undefined; - - // Handle both letsencrypt & custom cases - if (domainCertResolver === "custom") { - resolverName = domainCustomResolver?.trim(); - } else if (domainCertResolver) { - resolverName = domainCertResolver; - } else { - resolverName = globalDefaultResolver; - } - let tls = {}; if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { const domainParts = fullDomain.split("."); diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index 54cad172..f9a9dcbd 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -25,8 +25,8 @@ const bodySchema = z .object({ type: z.enum(["ns", "cname", "wildcard"]), baseDomain: subdomainSchema, - certResolver: z.enum(["letsencrypt", "custom"]).optional(), // optional, only for wildcard - customCertResolver: z.string().optional() // required if certResolver === "custom" + certResolver: z.string().optional().nullable(), + preferWildcardCert: z.boolean().optional().nullable() // optional, only for wildcard }) .strict(); @@ -38,7 +38,7 @@ export type CreateDomainResponse = { aRecords?: { baseDomain: string; value: string }[]; txtRecords?: { baseDomain: string; value: string }[]; certResolver?: string | null; - customCertResolver?: string | null; + preferWildcardCert?: boolean; }; // Helper to check if a domain is a subdomain or equal to another domain @@ -76,7 +76,7 @@ export async function createOrgDomain( } const { orgId } = parsedParams.data; - const { type, baseDomain, certResolver, customCertResolver } = parsedBody.data; + const { type, baseDomain, certResolver, preferWildcardCert } = parsedBody.data; if (build == "oss") { if (type !== "wildcard") { @@ -261,7 +261,7 @@ export async function createOrgDomain( type, verified: type === "wildcard" ? true : false, certResolver: certResolver || null, - customCertResolver: customCertResolver || null + preferWildcardCert: preferWildcardCert || false }) .returning(); @@ -334,7 +334,7 @@ export async function createOrgDomain( nsRecords, aRecords, certResolver: returned.certResolver, - customCertResolver: returned.customCertResolver + preferWildcardCert: returned.preferWildcardCert }, success: true, error: false, diff --git a/server/routers/domain/listDomains.ts b/server/routers/domain/listDomains.ts index 85bc3caa..55ea99cb 100644 --- a/server/routers/domain/listDomains.ts +++ b/server/routers/domain/listDomains.ts @@ -44,7 +44,7 @@ async function queryDomains(orgId: string, limit: number, offset: number) { tries: domains.tries, configManaged: domains.configManaged, certResolver: domains.certResolver, - customCertResolver: domains.customCertResolver, + preferWildcardCert: domains.preferWildcardCert }) .from(orgDomains) .where(eq(orgDomains.orgId, orgId)) diff --git a/server/setup/copyInConfig.ts b/server/setup/copyInConfig.ts index b8c00192..a8627d5e 100644 --- a/server/setup/copyInConfig.ts +++ b/server/setup/copyInConfig.ts @@ -37,7 +37,9 @@ async function copyInDomains() { const configDomains = Object.entries(rawDomains).map( ([key, value]) => ({ domainId: key, - baseDomain: value.base_domain.toLowerCase() + baseDomain: value.base_domain.toLowerCase(), + certResolver: value.cert_resolver || null, + preferWildcardCert: value.prefer_wildcard_cert || null }) ); @@ -59,11 +61,11 @@ async function copyInDomains() { } } - for (const { domainId, baseDomain } of configDomains) { + for (const { domainId, baseDomain, certResolver, preferWildcardCert } of configDomains) { if (existingDomainKeys.has(domainId)) { await trx .update(domains) - .set({ baseDomain, verified: true, type: "wildcard" }) + .set({ baseDomain, verified: true, type: "wildcard", certResolver, preferWildcardCert }) .where(eq(domains.domainId, domainId)) .execute(); } else { @@ -74,7 +76,9 @@ async function copyInDomains() { baseDomain, configManaged: true, type: "wildcard", - verified: true + verified: true, + certResolver, + preferWildcardCert }) .execute(); } diff --git a/src/components/CreateDomainForm.tsx b/src/components/CreateDomainForm.tsx index 24b1d466..7e7fcc66 100644 --- a/src/components/CreateDomainForm.tsx +++ b/src/components/CreateDomainForm.tsx @@ -11,6 +11,7 @@ import { FormDescription } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; +import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; import { useToast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; import { useState, useMemo } from "react"; @@ -98,8 +99,8 @@ const formSchema = z.object({ .refine((val) => isValidDomainFormat(val), "Invalid domain format") .transform((val) => toPunycode(val)), type: z.enum(["ns", "cname", "wildcard"]), - certResolver: z.string().optional(), - customCertResolver: z.string().optional() + certResolver: z.string().nullable().optional(), + preferWildcardCert: z.boolean().optional() }); type FormValues = z.infer; @@ -112,7 +113,7 @@ type CreateDomainFormProps = { // Example cert resolver options (replace with real API/fetch if needed) const certResolverOptions = [ - { id: "letsencrypt", title: "Let's Encrypt" }, + { id: "default", title: "Default" }, { id: "custom", title: "Custom Resolver" } ]; @@ -135,8 +136,8 @@ export default function CreateDomainForm({ defaultValues: { baseDomain: "", type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns", - certResolver: "", - customCertResolver: "" + certResolver: null, + preferWildcardCert: false } }); @@ -281,8 +282,20 @@ export default function CreateDomainForm({ {t("certResolver")} - {field.value === "custom" && ( - - - form.setValue("customCertResolver", e.target.value) - } + {field.value !== null && field.value !== "default" && ( +
+ + field.onChange(e.target.value)} + /> + + ( + + + + + {/*
+ + {t("preferWildcardCert")} + +
*/} +
+ )} /> - +
)} )} /> - )} From df24525105cb25b479760d2f99ca3ee6b7a9a07b Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 8 Oct 2025 14:48:21 -0700 Subject: [PATCH 06/38] Fix type issues --- server/routers/domain/createOrgDomain.ts | 2 +- server/setup/copyInConfig.ts | 2 +- src/components/DomainsTable.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index f9a9dcbd..b35a3de8 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -38,7 +38,7 @@ export type CreateDomainResponse = { aRecords?: { baseDomain: string; value: string }[]; txtRecords?: { baseDomain: string; value: string }[]; certResolver?: string | null; - preferWildcardCert?: boolean; + preferWildcardCert?: boolean | null; }; // Helper to check if a domain is a subdomain or equal to another domain diff --git a/server/setup/copyInConfig.ts b/server/setup/copyInConfig.ts index a8627d5e..e003d089 100644 --- a/server/setup/copyInConfig.ts +++ b/server/setup/copyInConfig.ts @@ -39,7 +39,7 @@ async function copyInDomains() { domainId: key, baseDomain: value.base_domain.toLowerCase(), certResolver: value.cert_resolver || null, - preferWildcardCert: value.prefer_wildcard_cert || null + preferWildcardCert: value.prefer_wildcard_cert || null, }) ); diff --git a/src/components/DomainsTable.tsx b/src/components/DomainsTable.tsx index 87d869df..02f1854d 100644 --- a/src/components/DomainsTable.tsx +++ b/src/components/DomainsTable.tsx @@ -25,7 +25,7 @@ export type DomainRow = { tries: number; configManaged: boolean; certResolver: string; - customCertResolver: string; + preferWildcardCert: boolean; }; type Props = { From 156fe529b5dd19946afd97c08d3c7a66db5901c6 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Wed, 15 Oct 2025 19:38:09 +0530 Subject: [PATCH 07/38] fix code conflicts and match dev change --- server/lib/traefik/getTraefikConfig.ts | 82 ++++++++++--------- .../private/lib/traefik/getTraefikConfig.ts | 4 +- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index 45729a30..74080f6f 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -15,6 +15,7 @@ import config from "@server/lib/config"; import { resources, sites, Target, targets } from "@server/db"; import createPathRewriteMiddleware from "./middleware"; import { sanitize, validatePathRewriteConfig } from "./utils"; +import { privateConfig } from "../../private/lib/config"; const redirectHttpsMiddlewareName = "redirect-to-https"; const badgerMiddlewareName = "badger"; @@ -253,36 +254,39 @@ export async function getTraefikConfig( const domainCertResolver = resource.domainCertResolver; const preferWildcardCert = resource.preferWildcardCert; - let resolverName: string | undefined; - let preferWildcard: boolean | undefined; - // Handle both letsencrypt & custom cases - if (domainCertResolver) { - resolverName = domainCertResolver.trim(); - } else { - resolverName = globalDefaultResolver; - } - - if ( - preferWildcardCert !== undefined && - preferWildcardCert !== null - ) { - preferWildcard = preferWildcardCert; - } else { - preferWildcard = globalDefaultPreferWildcard; - } let tls = {}; - if (build == "oss") { + if (!privateConfig.getRawPrivateConfig().flags.generate_own_certificates) { + + let resolverName: string | undefined; + let preferWildcard: boolean | undefined; + // Handle both letsencrypt & custom cases + if (domainCertResolver) { + resolverName = domainCertResolver.trim(); + } else { + resolverName = globalDefaultResolver; + } + + if ( + preferWildcardCert !== undefined && + preferWildcardCert !== null + ) { + preferWildcard = preferWildcardCert; + } else { + preferWildcard = globalDefaultPreferWildcard; + } + + tls = { certResolver: resolverName, ...(preferWildcard ? { - domains: [ - { - main: wildCard - } - ] - } + domains: [ + { + main: wildCard + } + ] + } : {}) }; } @@ -524,14 +528,14 @@ export async function getTraefikConfig( })(), ...(resource.stickySession ? { - sticky: { - cookie: { - name: "p_sticky", // TODO: make this configurable via config.yml like other cookies - secure: resource.ssl, - httpOnly: true - } - } - } + sticky: { + cookie: { + name: "p_sticky", // TODO: make this configurable via config.yml like other cookies + secure: resource.ssl, + httpOnly: true + } + } + } : {}) } }; @@ -632,13 +636,13 @@ export async function getTraefikConfig( })(), ...(resource.stickySession ? { - sticky: { - ipStrategy: { - depth: 0, - sourcePort: true - } - } - } + sticky: { + ipStrategy: { + depth: 0, + sourcePort: true + } + } + } : {}) } }; diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index c0f934cd..a74c2ec0 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -304,6 +304,8 @@ export async function getTraefikConfig( wildCard = resource.fullDomain; } + const configDomain = config.getDomain(resource.domainId); + let tls = {}; if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { const domainParts = fullDomain.split("."); @@ -331,7 +333,7 @@ export async function getTraefikConfig( } tls = { - certResolver: resolverName, + certResolver: certResolver, ...(preferWildcardCert ? { domains: [ From 9d452efc7d3f996862995814efc719ab354cd370 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Wed, 15 Oct 2025 19:52:31 +0530 Subject: [PATCH 08/38] fix treafik config mismatch --- server/lib/traefik/getTraefikConfig.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index 74080f6f..a5552c8c 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -254,11 +254,7 @@ export async function getTraefikConfig( const domainCertResolver = resource.domainCertResolver; const preferWildcardCert = resource.preferWildcardCert; - - let tls = {}; - if (!privateConfig.getRawPrivateConfig().flags.generate_own_certificates) { - - let resolverName: string | undefined; + let resolverName: string | undefined; let preferWildcard: boolean | undefined; // Handle both letsencrypt & custom cases if (domainCertResolver) { @@ -276,8 +272,7 @@ export async function getTraefikConfig( preferWildcard = globalDefaultPreferWildcard; } - - tls = { + const tls = { certResolver: resolverName, ...(preferWildcard ? { @@ -289,7 +284,7 @@ export async function getTraefikConfig( } : {}) }; - } + const additionalMiddlewares = config.getRawConfig().traefik.additional_middlewares || []; From f10271890144224cba8f9e2e4025368ecd5b2b47 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Thu, 16 Oct 2025 16:27:31 +0530 Subject: [PATCH 09/38] add edit button to domain table --- src/app/[orgId]/settings/domains/page.tsx | 2 +- src/components/DomainsTable.tsx | 54 ++++++++++++++++++++--- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/src/app/[orgId]/settings/domains/page.tsx b/src/app/[orgId]/settings/domains/page.tsx index cb587d92..2c667b3a 100644 --- a/src/app/[orgId]/settings/domains/page.tsx +++ b/src/app/[orgId]/settings/domains/page.tsx @@ -60,7 +60,7 @@ export default async function DomainsPage(props: Props) { title={t("domains")} description={t("domainsDescription")} /> - + ); diff --git a/src/components/DomainsTable.tsx b/src/components/DomainsTable.tsx index 02f1854d..51aa951a 100644 --- a/src/components/DomainsTable.tsx +++ b/src/components/DomainsTable.tsx @@ -3,7 +3,7 @@ import { ColumnDef } from "@tanstack/react-table"; import { DomainsDataTable } from "@app/components/DomainsDataTable"; import { Button } from "@app/components/ui/button"; -import { ArrowUpDown } from "lucide-react"; +import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react"; import { useState } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { formatAxiosError } from "@app/lib/api"; @@ -15,6 +15,8 @@ import { useTranslations } from "next-intl"; import CreateDomainForm from "@app/components/CreateDomainForm"; import { useToast } from "@app/hooks/useToast"; import { useOrgContext } from "@app/hooks/useOrgContext"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; +import Link from "next/link"; export type DomainRow = { domainId: string; @@ -30,9 +32,10 @@ export type DomainRow = { type Props = { domains: DomainRow[]; + orgId: string; }; -export default function DomainsTable({ domains }: Props) { +export default function DomainsTable({ domains, orgId }: Props) { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); const [selectedDomain, setSelectedDomain] = useState( @@ -207,12 +210,51 @@ export default function DomainsTable({ domains }: Props) { > {isRestarting ? t("restarting", { - fallback: "Restarting..." - }) + fallback: "Restarting..." + }) : t("restart", { fallback: "Restart" })} )} - + + + + + {t("viewSettings")} + + + { + setSelectedDomain(domain); + setIsDeleteModalOpen(true); + }} + > + + {t("delete")} + + + + + + + + + + {/* + */} ); } From ae670e1eb54b3ad5fa94e43f77a89753177b4e4d Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Thu, 16 Oct 2025 19:11:29 +0530 Subject: [PATCH 10/38] initial setup for viewing domain details --- messages/en-US.json | 5 +- server/auth/actions.ts | 1 + server/routers/domain/getDomain.ts | 86 +++++++++++++++++++ server/routers/domain/index.ts | 3 +- server/routers/external.ts | 7 ++ .../settings/domains/[domainId]/layout.tsx | 50 +++++++++++ .../settings/domains/[domainId]/page.tsx | 8 ++ src/components/DomainInfoCard.tsx | 59 +++++++++++++ src/contexts/domainContext.ts | 11 +++ src/hooks/useDomainContext.ts | 10 +++ src/providers/DomainProvider.tsx | 43 ++++++++++ 11 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 server/routers/domain/getDomain.ts create mode 100644 src/app/[orgId]/settings/domains/[domainId]/layout.tsx create mode 100644 src/app/[orgId]/settings/domains/[domainId]/page.tsx create mode 100644 src/components/DomainInfoCard.tsx create mode 100644 src/contexts/domainContext.ts create mode 100644 src/hooks/useDomainContext.ts create mode 100644 src/providers/DomainProvider.tsx diff --git a/messages/en-US.json b/messages/en-US.json index c3e93ba8..a60e432c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1896,5 +1896,8 @@ "certResolverDescription": "Select the certificate resolver to use for this resource.", "selectCertResolver": "Select Certificate Resolver", "enterCustomResolver": "Enter Custom Resolver", - "preferWildcardCert": "Prefer Wildcard Certificate" + "preferWildcardCert": "Prefer Wildcard Certificate", + "unverified": "Unverified", + "domainSetting": "DomainSetting", + "domainSettingDescription": "Configure settings for your domain" } diff --git a/server/auth/actions.ts b/server/auth/actions.ts index e48bc502..4e2738e1 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -81,6 +81,7 @@ export enum ActionsEnum { listClients = "listClients", getClient = "getClient", listOrgDomains = "listOrgDomains", + getDomain = "getDomain", createNewt = "createNewt", createIdp = "createIdp", updateIdp = "updateIdp", diff --git a/server/routers/domain/getDomain.ts b/server/routers/domain/getDomain.ts new file mode 100644 index 00000000..77bd18ae --- /dev/null +++ b/server/routers/domain/getDomain.ts @@ -0,0 +1,86 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, domains } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { domain } from "zod/v4/core/regexes"; + +const getDomainSchema = z + .object({ + domainId: z + .string() + .optional(), + orgId: z.string().optional() + }) + .strict(); + +async function query(domainId?: string, orgId?: string) { + if (domainId) { + const [res] = await db + .select() + .from(domains) + .where(eq(domains.domainId, domainId)) + .limit(1); + return res; + } +} + +export type GetDomainResponse = NonNullable>>; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/domain/{domainId}", + description: "Get a domain by domainId.", + tags: [OpenAPITags.Domain], + request: { + params: z.object({ + domainId: z.string(), + orgId: z.string() + }) + }, + responses: {} +}); + +export async function getDomain( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getDomainSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId, domainId } = parsedParams.data; + + const domain = await query(domainId, orgId); + + if (!domain) { + return next(createHttpError(HttpCode.NOT_FOUND, "Domain not found")); + } + + return response(res, { + data: domain, + success: true, + error: false, + message: "Domain retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/domain/index.ts b/server/routers/domain/index.ts index c0cafafe..e131e6a4 100644 --- a/server/routers/domain/index.ts +++ b/server/routers/domain/index.ts @@ -1,4 +1,5 @@ export * from "./listDomains"; export * from "./createOrgDomain"; export * from "./deleteOrgDomain"; -export * from "./restartOrgDomain"; \ No newline at end of file +export * from "./restartOrgDomain"; +export * from "./getDomain"; \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index 8bd72f62..cadbbad7 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -302,6 +302,13 @@ authenticated.get( domain.listDomains ); +authenticated.get( + "/org/:orgId/domain/:domainId", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getDomain), + domain.getDomain +); + authenticated.get( "/org/:orgId/invitations", verifyOrgAccess, diff --git a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx new file mode 100644 index 00000000..1e1fec7b --- /dev/null +++ b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx @@ -0,0 +1,50 @@ +import { internal } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { redirect } from "next/navigation"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { getTranslations } from "next-intl/server"; +import { GetDomainResponse } from "@server/routers/domain/getDomain"; +import DomainProvider from "@app/providers/DomainProvider"; +import DomainInfoCard from "@app/components/DomainInfoCard"; + +interface SettingsLayoutProps { + children: React.ReactNode; + params: Promise<{ domainId: string; orgId: string }>; +} + +export default async function SettingsLayout(props: SettingsLayoutProps) { + const params = await props.params; + + const { children } = props; + + let domain = null; + try { + const res = await internal.get>( + `/org/${params.orgId}/domain/${params.domainId}`, + await authCookieHeader() + ); + domain = res.data.data; + console.log(JSON.stringify(domain)); + } catch { + redirect(`/${params.orgId}/settings/domains`); + } + + const t = await getTranslations(); + + + return ( + <> + + + +
+ +
+
+ + ); +} diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx new file mode 100644 index 00000000..e8187b95 --- /dev/null +++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx @@ -0,0 +1,8 @@ +import { redirect } from "next/navigation"; + +export default async function DomainPage(props: { + params: Promise<{ orgId: string; domainId: string }>; +}) { + const params = await props.params; + redirect(`/${params.orgId}/settings/domains/${params.domainId}`); +} \ No newline at end of file diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx new file mode 100644 index 00000000..951633f2 --- /dev/null +++ b/src/components/DomainInfoCard.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { InfoIcon } from "lucide-react"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import { useTranslations } from "next-intl"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useDomainContext } from "@app/hooks/useDomainContext"; + +type DomainInfoCardProps = {}; + +export default function DomainInfoCard({ }: DomainInfoCardProps) { + const { domain, updateDomain } = useDomainContext(); + const t = useTranslations(); + const { env } = useEnvContext(); + + + return ( + + + + + + {t("type")} + + + + {domain.type} + + + + + + {t("status")} + + + {domain.verified ? ( +
+
+ {t("verified")} +
+ ) : ( +
+
+ {t("unverified")} +
+ )} +
+
+
+
+
+ ); +} diff --git a/src/contexts/domainContext.ts b/src/contexts/domainContext.ts new file mode 100644 index 00000000..d60c4ca4 --- /dev/null +++ b/src/contexts/domainContext.ts @@ -0,0 +1,11 @@ +import { GetDomainResponse } from "@server/routers/domain/getDomain"; +import { createContext } from "react"; + +interface DomainContextType { + domain: GetDomainResponse; + updateDomain: (updatedDomain: Partial) => void; +} + +const DomainContext = createContext(undefined); + +export default DomainContext; \ No newline at end of file diff --git a/src/hooks/useDomainContext.ts b/src/hooks/useDomainContext.ts new file mode 100644 index 00000000..36d3840f --- /dev/null +++ b/src/hooks/useDomainContext.ts @@ -0,0 +1,10 @@ +import DomainContext from "@app/contexts/domainContext"; +import { useContext } from "react"; + +export function useDomainContext() { + const context = useContext(DomainContext); + if (context === undefined) { + throw new Error('useDomainContext must be used within a DomainProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/providers/DomainProvider.tsx b/src/providers/DomainProvider.tsx new file mode 100644 index 00000000..9b014449 --- /dev/null +++ b/src/providers/DomainProvider.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { GetDomainResponse } from "@server/routers/domain/getDomain"; +import DomainContext from "@app/contexts/domainContext"; + +interface DomainProviderProps { + children: React.ReactNode; + domain: GetDomainResponse; +} + +export function DomainProvider({ + children, + domain: serverDomain +}: DomainProviderProps) { + const [domain, setDomain] = useState(serverDomain); + + const t = useTranslations(); + + const updateDomain = (updatedDomain: Partial) => { + if (!domain) { + throw new Error(t('domainErrorNoUpdate')); + } + setDomain((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + ...updatedDomain + }; + }); + }; + + return ( + + {children} + + ); +} + +export default DomainProvider; \ No newline at end of file From 43f907ebeccb998160256310040478142c880bc3 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Thu, 16 Oct 2025 21:32:25 +0530 Subject: [PATCH 11/38] remove import --- server/lib/traefik/getTraefikConfig.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index a5552c8c..da2f2001 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -15,7 +15,6 @@ import config from "@server/lib/config"; import { resources, sites, Target, targets } from "@server/db"; import createPathRewriteMiddleware from "./middleware"; import { sanitize, validatePathRewriteConfig } from "./utils"; -import { privateConfig } from "../../private/lib/config"; const redirectHttpsMiddlewareName = "redirect-to-https"; const badgerMiddlewareName = "badger"; From a9b9161c40ec2fb6b4188f531d499df540c267f3 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Thu, 16 Oct 2025 23:37:55 +0530 Subject: [PATCH 12/38] template for Domain Settings --- messages/en-US.json | 5 +- src/components/DomainInfoCard.tsx | 269 ++++++++++++++++++++++++++---- 2 files changed, 238 insertions(+), 36 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index a60e432c..b2dbb302 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1898,6 +1898,7 @@ "enterCustomResolver": "Enter Custom Resolver", "preferWildcardCert": "Prefer Wildcard Certificate", "unverified": "Unverified", - "domainSetting": "DomainSetting", - "domainSettingDescription": "Configure settings for your domain" + "domainSetting": "Domain Settings", + "domainSettingDescription": "Configure settings for your domain", + "preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver)." } diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx index 951633f2..8564acfa 100644 --- a/src/components/DomainInfoCard.tsx +++ b/src/components/DomainInfoCard.tsx @@ -11,49 +11,250 @@ 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 { Button } from "./ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormDescription +} from "@app/components/ui/form"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; +import { Input } from "./ui/input"; +import { CheckboxWithLabel } from "./ui/checkbox"; +import { useForm } from "react-hook-form"; +import z from "zod"; +import { toASCII } from "punycode"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { build } from "@server/build"; +import { Switch } from "./ui/switch"; type DomainInfoCardProps = {}; +// Helper functions for Unicode domain handling +function toPunycode(domain: string): string { + try { + const parts = toASCII(domain); + return parts; + } catch (error) { + return domain.toLowerCase(); + } +} + + +function isValidDomainFormat(domain: string): boolean { + const unicodeRegex = /^(?!:\/\/)([^\s.]+\.)*[^\s.]+$/; + + if (!unicodeRegex.test(domain)) { + return false; + } + + const parts = domain.split('.'); + for (const part of parts) { + if (part.length === 0 || part.startsWith('-') || part.endsWith('-')) { + return false; + } + if (part.length > 63) { + return false; + } + } + + if (domain.length > 253) { + return false; + } + + return true; +} + +const formSchema = z.object({ + baseDomain: z + .string() + .min(1, "Domain is required") + .refine((val) => isValidDomainFormat(val), "Invalid domain format") + .transform((val) => toPunycode(val)), + type: z.enum(["ns", "cname", "wildcard"]), + certResolver: z.string().nullable().optional(), + preferWildcardCert: z.boolean().optional() +}); + +type FormValues = z.infer; + +const certResolverOptions = [ + { id: "default", title: "Default" }, + { id: "custom", title: "Custom Resolver" } +]; + + export default function DomainInfoCard({ }: DomainInfoCardProps) { const { domain, updateDomain } = useDomainContext(); const t = useTranslations(); const { env } = useEnvContext(); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + baseDomain: "", + type: build == "oss" || !env.flags.usePangolinDns ? "wildcard" : "ns", + certResolver: null, + preferWildcardCert: false + } + }); + return ( - - - - - - {t("type")} - - - - {domain.type} - - - - - - {t("status")} - - - {domain.verified ? ( -
-
- {t("verified")} -
- ) : ( -
-
- {t("unverified")} -
- )} -
-
-
-
-
+ <> + + + + + + {t("type")} + + + + {domain.type} + + + + + + {t("status")} + + + {domain.verified ? ( +
+
+ {t("verified")} +
+ ) : ( +
+
+ {t("unverified")} +
+ )} +
+
+
+
+
+ + + + + + {/* Domain Settings */} + {/* Add condition later to only show when domain is wildcard */} + + + + + {t("domainSetting")} + + + + + +
+ + ( + + {t("certResolver")} + + + + + {field.value !== null && field.value !== "default" && ( +
+ + field.onChange(e.target.value)} + /> + + ( + + +
+ + {t("preferWildcardCert")} +
+
+ + + {t("preferWildcardCertDescription")} + + +
+ )} + /> +
+ )} +
+ )} + /> + + +
+
+ + + + +
+
+ ); } From 8fdf120ec2f04127ed8dcfa1be2998dab737ea6e Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Fri, 17 Oct 2025 22:25:19 +0530 Subject: [PATCH 13/38] backend setup to store and get DNS Records --- server/auth/actions.ts | 1 + server/db/sqlite/schema/schema.ts | 13 ++++ server/routers/domain/createOrgDomain.ts | 43 +++++++++++- server/routers/domain/getDNSRecords.ts | 86 ++++++++++++++++++++++++ server/routers/domain/index.ts | 3 +- server/routers/external.ts | 7 ++ 6 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 server/routers/domain/getDNSRecords.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 4e2738e1..4c442d2c 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -82,6 +82,7 @@ export enum ActionsEnum { getClient = "getClient", listOrgDomains = "listOrgDomains", getDomain = "getDomain", + getDNSRecords = "getDNSRecords", createNewt = "createNewt", createIdp = "createIdp", updateIdp = "updateIdp", diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 30841a4b..bc1ce81b 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -16,6 +16,18 @@ export const domains = sqliteTable("domains", { preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }) }); +export const dnsRecords = sqliteTable("dnsRecords", { + id: text("id").primaryKey(), + domainId: text("domainId") + .notNull() + .references(() => domains.domainId, { onDelete: "cascade" }), + + recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT" + baseDomain: text("baseDomain"), + value: text("value").notNull(), +}); + + export const orgs = sqliteTable("orgs", { orgId: text("orgId").primaryKey(), name: text("name").notNull(), @@ -748,6 +760,7 @@ export type ResourceWhitelist = InferSelectModel; export type VersionMigration = InferSelectModel; export type ResourceRule = InferSelectModel; export type Domain = InferSelectModel; +export type DnsRecord = InferSelectModel; export type Client = InferSelectModel; export type ClientSite = InferSelectModel; export type RoleClient = InferSelectModel; diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index b35a3de8..1f1002af 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, Domain, domains, OrgDomains, orgDomains } from "@server/db"; +import { db, Domain, domains, OrgDomains, orgDomains, dnsRecords } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -276,9 +276,23 @@ export async function createOrgDomain( }) .returning(); + // Prepare DNS records to insert + const recordsToInsert = []; + // TODO: This needs to be cross region and not hardcoded if (type === "ns") { nsRecords = config.getRawConfig().dns.nameservers as string[]; + + // Save NS records to database + for (const nsValue of nsRecords) { + recordsToInsert.push({ + id: generateId(15), + domainId, + recordType: "NS", + baseDomain: baseDomain, + value: nsValue + }); + } } else if (type === "cname") { cnameRecords = [ { @@ -290,6 +304,17 @@ export async function createOrgDomain( baseDomain: `_acme-challenge.${baseDomain}` } ]; + + // Save CNAME records to database + for (const cnameRecord of cnameRecords) { + recordsToInsert.push({ + id: generateId(15), + domainId, + recordType: "CNAME", + baseDomain: cnameRecord.baseDomain, + value: cnameRecord.value + }); + } } else if (type === "wildcard") { aRecords = [ { @@ -301,6 +326,22 @@ export async function createOrgDomain( baseDomain: `${baseDomain}` } ]; + + // Save A records to database + for (const aRecord of aRecords) { + recordsToInsert.push({ + id: generateId(15), + domainId, + recordType: "A", + baseDomain: aRecord.baseDomain, + value: aRecord.value + }); + } + } + + // Insert all DNS records in batch + if (recordsToInsert.length > 0) { + await trx.insert(dnsRecords).values(recordsToInsert); } numOrgDomains = await trx diff --git a/server/routers/domain/getDNSRecords.ts b/server/routers/domain/getDNSRecords.ts new file mode 100644 index 00000000..ee349cdd --- /dev/null +++ b/server/routers/domain/getDNSRecords.ts @@ -0,0 +1,86 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, dnsRecords } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const getDNSRecordsSchema = z + .object({ + domainId: z.string(), + orgId: z.string() + }) + .strict(); + +async function query(domainId: string) { + const records = await db + .select() + .from(dnsRecords) + .where(eq(dnsRecords.domainId, domainId)); + + return records; +} + +export type GetDNSRecordsResponse = Awaited>; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/domain/{domainId}/dns-records", + description: "Get all DNS records for a domain by domainId.", + tags: [OpenAPITags.Domain], + request: { + params: z.object({ + domainId: z.string(), + orgId: z.string() + }) + }, + responses: {} +}); + +export async function getDNSRecords( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getDNSRecordsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { domainId } = parsedParams.data; + + const records = await query(domainId); + + if (!records || records.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "No DNS records found for this domain" + ) + ); + } + + return response(res, { + data: records, + success: true, + error: false, + message: "DNS records retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} \ No newline at end of file diff --git a/server/routers/domain/index.ts b/server/routers/domain/index.ts index e131e6a4..0bfedb41 100644 --- a/server/routers/domain/index.ts +++ b/server/routers/domain/index.ts @@ -2,4 +2,5 @@ export * from "./listDomains"; export * from "./createOrgDomain"; export * from "./deleteOrgDomain"; export * from "./restartOrgDomain"; -export * from "./getDomain"; \ No newline at end of file +export * from "./getDomain"; +export * from "./getDNSRecords"; \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index cadbbad7..c00f1e9f 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -309,6 +309,13 @@ authenticated.get( domain.getDomain ); +authenticated.get( + "/org/:orgId/domain/:domainId/dns-records", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getDNSRecords), + domain.getDNSRecords +); + authenticated.get( "/org/:orgId/invitations", verifyOrgAccess, From c29ba9bb5ff04fbe1be24412c307a56e8ecfc187 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sat, 18 Oct 2025 01:12:02 +0530 Subject: [PATCH 14/38] add DNS Records table --- messages/en-US.json | 5 +- server/routers/domain/createOrgDomain.ts | 9 +- .../settings/domains/[domainId]/layout.tsx | 2 +- src/components/DNSRecordTable.tsx | 138 ++++++++++++ src/components/DNSRecordsDataTable.tsx | 209 ++++++++++++++++++ src/components/DomainInfoCard.tsx | 80 ++++++- 6 files changed, 426 insertions(+), 17 deletions(-) create mode 100644 src/components/DNSRecordTable.tsx create mode 100644 src/components/DNSRecordsDataTable.tsx diff --git a/messages/en-US.json b/messages/en-US.json index b2dbb302..24e86a36 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1900,5 +1900,8 @@ "unverified": "Unverified", "domainSetting": "Domain Settings", "domainSettingDescription": "Configure settings for your domain", - "preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver)." + "preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver).", + "recordName": "Record Name", + "auto": "Auto", + "TTL": "TTL" } diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts index 1f1002af..d40a0cb8 100644 --- a/server/routers/domain/createOrgDomain.ts +++ b/server/routers/domain/createOrgDomain.ts @@ -290,7 +290,8 @@ export async function createOrgDomain( domainId, recordType: "NS", baseDomain: baseDomain, - value: nsValue + value: nsValue, + verified: false }); } } else if (type === "cname") { @@ -312,7 +313,8 @@ export async function createOrgDomain( domainId, recordType: "CNAME", baseDomain: cnameRecord.baseDomain, - value: cnameRecord.value + value: cnameRecord.value, + verified: false }); } } else if (type === "wildcard") { @@ -334,7 +336,8 @@ export async function createOrgDomain( domainId, recordType: "A", baseDomain: aRecord.baseDomain, - value: aRecord.value + value: aRecord.value, + verified: true }); } } diff --git a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx index 1e1fec7b..319ccdd5 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx @@ -42,7 +42,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
- +
diff --git a/src/components/DNSRecordTable.tsx b/src/components/DNSRecordTable.tsx new file mode 100644 index 00000000..dd9bcad4 --- /dev/null +++ b/src/components/DNSRecordTable.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { Button } from "@app/components/ui/button"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { useToast } from "@app/hooks/useToast"; +import { Badge } from "@app/components/ui/badge"; +import CopyToClipboard from "@app/components/CopyToClipboard"; +import { DNSRecordsDataTable } from "./DNSRecordsDataTable"; + +export type DNSRecordRow = { + id: string; + domainId: string; + recordType: string; // "NS" | "CNAME" | "A" | "TXT" + baseDomain: string | null; + value: string; + verified?: boolean; +}; + +type Props = { + records: DNSRecordRow[]; + domainId: string; + isRefreshing?: boolean; +}; + +export default function DNSRecordsTable({ records, domainId, isRefreshing }: Props) { + const t = useTranslations(); + + const columns: ColumnDef[] = [ + { + accessorKey: "baseDomain", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const baseDomain = row.original.baseDomain; + return ( +
+ {baseDomain || "-"} +
+ ); + } + }, + { + accessorKey: "recordType", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const type = row.original.recordType; + return ( +
+ {type} +
+ ); + } + }, + { + accessorKey: "ttl", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return ( +
+ {t("auto")} +
+ ); + } + }, + { + accessorKey: "value", + header: () => { + return
{t("value")}
; + }, + cell: ({ row }) => { + const value = row.original.value; + return ( +
+
+ {value} +
+
+ ); + } + }, + { + accessorKey: "verified", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + const verified = row.original.verified; + return ( + verified ? ( + {t("verified")} + ) : ( + {t("unverified")} + ) + ); + } + } + ]; + + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/DNSRecordsDataTable.tsx b/src/components/DNSRecordsDataTable.tsx new file mode 100644 index 00000000..4e70a1b2 --- /dev/null +++ b/src/components/DNSRecordsDataTable.tsx @@ -0,0 +1,209 @@ +"use client"; + +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + getSortedRowModel, + getFilteredRowModel +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@/components/ui/table"; +import { Button } from "@app/components/ui/button"; +import { useMemo, useState } from "react"; +import { Plus, RefreshCw } from "lucide-react"; +import { + Card, + CardContent, + CardHeader, + CardTitle +} from "@app/components/ui/card"; +import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs"; +import { useTranslations } from "next-intl"; +import { Badge } from "./ui/badge"; + + +type TabFilter = { + id: string; + label: string; + filterFn: (row: any) => boolean; +}; + +type DNSRecordsDataTableProps = { + columns: ColumnDef[]; + data: TData[]; + title?: string; + addButtonText?: string; + onAdd?: () => void; + onRefresh?: () => void; + isRefreshing?: boolean; + searchPlaceholder?: string; + searchColumn?: string; + defaultSort?: { + id: string; + desc: boolean; + }; + tabs?: TabFilter[]; + defaultTab?: string; + persistPageSize?: boolean | string; + defaultPageSize?: number; +}; + +export function DNSRecordsDataTable({ + columns, + data, + title, + addButtonText, + onAdd, + onRefresh, + isRefreshing, + defaultSort, + tabs, + defaultTab, + +}: DNSRecordsDataTableProps) { + const t = useTranslations(); + + const [activeTab, setActiveTab] = useState( + defaultTab || tabs?.[0]?.id || "" + ); + + // Apply tab filter to data + const filteredData = useMemo(() => { + if (!tabs || activeTab === "") { + return data; + } + + const activeTabFilter = tabs.find((tab) => tab.id === activeTab); + if (!activeTabFilter) { + return data; + } + + return data.filter(activeTabFilter.filterFn); + }, [data, tabs, activeTab]); + + const table = useReactTable({ + data: filteredData, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + }); + + + + + return ( +
+ + +
+
+

DNS Records

+ Required +
+ {tabs && tabs.length > 0 && ( + + + {tabs.map((tab) => ( + + {tab.label} ( + {data.filter(tab.filterFn).length}) + + ))} + + + )} +
+
+ {onRefresh && ( + + )} + {onAdd && addButtonText && ( + + )} +
+
+ + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results found. + + + )} + +
+
+
+
+ ); +} diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx index 8564acfa..f768a5c2 100644 --- a/src/components/DomainInfoCard.tsx +++ b/src/components/DomainInfoCard.tsx @@ -1,7 +1,6 @@ "use client"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; -import { InfoIcon } from "lucide-react"; import { InfoSection, InfoSectionContent, @@ -24,15 +23,23 @@ import { } from "@app/components/ui/form"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Input } from "./ui/input"; -import { CheckboxWithLabel } from "./ui/checkbox"; import { useForm } from "react-hook-form"; import z from "zod"; import { toASCII } from "punycode"; import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; import { Switch } from "./ui/switch"; +import DNSRecordsTable, { DNSRecordRow } from "./DNSrecordTable"; +import { useEffect, useState } from "react"; +import { createApiClient } from "@app/lib/api"; +import { useToast } from "@app/hooks/useToast"; +import { formatAxiosError } from "@app/lib/api"; +import { Badge } from "./ui/badge"; -type DomainInfoCardProps = {}; +type DomainInfoCardProps = { + orgId?: string; + domainId?: string; +}; // Helper functions for Unicode domain handling function toPunycode(domain: string): string { @@ -88,10 +95,16 @@ const certResolverOptions = [ ]; -export default function DomainInfoCard({ }: DomainInfoCardProps) { +export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) { const { domain, updateDomain } = useDomainContext(); const t = useTranslations(); const { env } = useEnvContext(); + const api = createApiClient(useEnvContext()); + const { toast } = useToast(); + + const [dnsRecords, setDnsRecords] = useState([]); + const [loadingRecords, setLoadingRecords] = useState(true); + const [isRefreshing, setIsRefreshing] = useState(false); const form = useForm({ resolver: zodResolver(formSchema), @@ -103,6 +116,40 @@ export default function DomainInfoCard({ }: DomainInfoCardProps) { } }); + const fetchDNSRecords = async (showRefreshing = false) => { + if (showRefreshing) { + setIsRefreshing(true); + } else { + setLoadingRecords(true); + } + + try { + const response = await api.get<{ data: DNSRecordRow[] }>( + `/org/${orgId}/domain/${domainId}/dns-records` + ); + setDnsRecords(response.data.data); + } catch (error) { + // Only show error if records exist (not a 404) + const err = error as any; + if (err?.response?.status !== 404) { + toast({ + title: t("error"), + description: formatAxiosError(error), + variant: "destructive" + }); + } + } finally { + setLoadingRecords(false); + setIsRefreshing(false); + } + }; + + useEffect(() => { + if (domain.domainId) { + fetchDNSRecords(); + } + }, [domain.domainId]); + return ( <> @@ -126,8 +173,9 @@ export default function DomainInfoCard({ }: DomainInfoCardProps) { {domain.verified ? (
-
- {t("verified")} + + {t("verified")} +
) : (
@@ -141,12 +189,20 @@ export default function DomainInfoCard({ }: DomainInfoCardProps) { + {loadingRecords ? ( +
+ loading... +
+ ) : ( + + )} - - - - {/* Domain Settings */} - {/* Add condition later to only show when domain is wildcard */} + {/* Domain Settings */} + {/* Add condition later to only show when domain is wildcard */} @@ -257,4 +313,4 @@ export default function DomainInfoCard({ }: DomainInfoCardProps) { ); -} +} \ No newline at end of file From 2c01849f2ed52824f34e206c12617c80e716b02d Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sat, 18 Oct 2025 01:17:25 +0530 Subject: [PATCH 15/38] fix import --- src/components/DomainInfoCard.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx index f768a5c2..0f967f33 100644 --- a/src/components/DomainInfoCard.tsx +++ b/src/components/DomainInfoCard.tsx @@ -29,8 +29,8 @@ import { toASCII } from "punycode"; import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; import { Switch } from "./ui/switch"; -import DNSRecordsTable, { DNSRecordRow } from "./DNSrecordTable"; import { useEffect, useState } from "react"; +import DNSRecordsTable, {DNSRecordRow} from "./DNSRecordTable"; import { createApiClient } from "@app/lib/api"; import { useToast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; From d37e28215ecc2c7535c379c3b0f5dd538c14acfc Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sat, 18 Oct 2025 16:15:21 +0530 Subject: [PATCH 16/38] add restart button --- .../[domainId]/DomainSettingsLayout.tsx | 112 ++++++++++++++++++ .../settings/domains/[domainId]/layout.tsx | 68 +++++------ 2 files changed, 140 insertions(+), 40 deletions(-) create mode 100644 src/app/[orgId]/settings/domains/[domainId]/DomainSettingsLayout.tsx diff --git a/src/app/[orgId]/settings/domains/[domainId]/DomainSettingsLayout.tsx b/src/app/[orgId]/settings/domains/[domainId]/DomainSettingsLayout.tsx new file mode 100644 index 00000000..3b822dc0 --- /dev/null +++ b/src/app/[orgId]/settings/domains/[domainId]/DomainSettingsLayout.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useState } from "react"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { useRouter } from "next/navigation"; +import { RefreshCw } from "lucide-react"; +import { Button } from "@app/components/ui/button"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import DomainInfoCard from "@app/components/DomainInfoCard"; +import DomainProvider from "@app/providers/DomainProvider"; +import { useTranslations } from "next-intl"; + + +interface DomainSettingsLayoutProps { + orgId: string; + domain: any, + children: React.ReactNode; +} + +export default function DomainSettingsLayout({ orgId, domain, children }: DomainSettingsLayoutProps) { + const router = useRouter(); + const api = createApiClient(useEnvContext()); + const [isRefreshing, setIsRefreshing] = useState(false); + const [restartingDomains, setRestartingDomains] = useState>(new Set()); + const t = useTranslations(); + 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; + }); + } + }; + + const isRestarting = restartingDomains.has(domain.domainId); + + return ( + <> +
+ + + +
+ + +
+ +
+ {children} +
+ + ); +} diff --git a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx index 319ccdd5..c75ceaf4 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx @@ -1,50 +1,38 @@ -import { internal } from "@app/lib/api"; -import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { getTranslations } from "next-intl/server"; +import { internal } from "@app/lib/api"; import { GetDomainResponse } from "@server/routers/domain/getDomain"; -import DomainProvider from "@app/providers/DomainProvider"; -import DomainInfoCard from "@app/components/DomainInfoCard"; +import { AxiosResponse } from "axios"; +import { getTranslations } from "next-intl/server"; +import SettingsLayoutClient from "./DomainSettingsLayout"; interface SettingsLayoutProps { - children: React.ReactNode; - params: Promise<{ domainId: string; orgId: string }>; + children: React.ReactNode; + params: Promise<{ domainId: string; orgId: string }>; } -export default async function SettingsLayout(props: SettingsLayoutProps) { - const params = await props.params; +export default async function SettingsLayout({ children, params }: SettingsLayoutProps) { + const { domainId, orgId } = await params; - const { children } = props; - - let domain = null; - try { - const res = await internal.get>( - `/org/${params.orgId}/domain/${params.domainId}`, - await authCookieHeader() - ); - domain = res.data.data; - console.log(JSON.stringify(domain)); - } catch { - redirect(`/${params.orgId}/settings/domains`); - } - - const t = await getTranslations(); - - - return ( - <> - - - -
- -
-
- + let domain = null; + try { + const res = await internal.get>( + `/org/${orgId}/domain/${domainId}`, + await authCookieHeader() ); + domain = res.data.data; + } catch { + redirect(`/${orgId}/settings/domains`); + } + + const t = await getTranslations(); + + return ( + + {children} + + ); } From 51af293d66a17458e0812b1d3ed826be7ec3fa1a Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sat, 18 Oct 2025 17:53:50 +0530 Subject: [PATCH 17/38] add doc link button and fix continuous polling --- messages/en-US.json | 3 +- .../settings/domains/[domainId]/page.tsx | 9 ++-- src/components/DNSRecordTable.tsx | 30 +++++------ src/components/DNSRecordsDataTable.tsx | 53 ++++--------------- src/components/DomainInfoCard.tsx | 2 +- 5 files changed, 29 insertions(+), 68 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 24e86a36..4d382869 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1903,5 +1903,6 @@ "preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver).", "recordName": "Record Name", "auto": "Auto", - "TTL": "TTL" + "TTL": "TTL", + "howToAddRecords": "How to Add Records" } diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx index e8187b95..791ba96d 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/page.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx @@ -1,8 +1,5 @@ -import { redirect } from "next/navigation"; -export default async function DomainPage(props: { - params: Promise<{ orgId: string; domainId: string }>; -}) { - const params = await props.params; - redirect(`/${params.orgId}/settings/domains/${params.domainId}`); + +export default function DomainPage({ children }: { children: React.ReactNode }) { + return <>{children}; } \ No newline at end of file diff --git a/src/components/DNSRecordTable.tsx b/src/components/DNSRecordTable.tsx index dd9bcad4..e0760977 100644 --- a/src/components/DNSRecordTable.tsx +++ b/src/components/DNSRecordTable.tsx @@ -15,7 +15,7 @@ export type DNSRecordRow = { recordType: string; // "NS" | "CNAME" | "A" | "TXT" baseDomain: string | null; value: string; - verified?: boolean; + verified?: boolean; }; type Props = { @@ -32,17 +32,16 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro accessorKey: "baseDomain", header: ({ column }) => { return ( - +
); }, cell: ({ row }) => { const baseDomain = row.original.baseDomain; return ( -
+
{baseDomain || "-"}
); @@ -52,11 +51,10 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro accessorKey: "recordType", header: ({ column }) => { return ( - +
); }, cell: ({ row }) => { @@ -72,11 +70,10 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro accessorKey: "ttl", header: ({ column }) => { return ( - + ); }, cell: ({ row }) => { @@ -95,10 +92,8 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro cell: ({ row }) => { const value = row.original.value; return ( -
-
- {value} -
+
+ {value}
); } @@ -107,11 +102,10 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro accessorKey: "verified", header: ({ column }) => { return ( - +
); }, cell: ({ row }) => { diff --git a/src/components/DNSRecordsDataTable.tsx b/src/components/DNSRecordsDataTable.tsx index 4e70a1b2..422cf318 100644 --- a/src/components/DNSRecordsDataTable.tsx +++ b/src/components/DNSRecordsDataTable.tsx @@ -19,7 +19,7 @@ import { } from "@/components/ui/table"; import { Button } from "@app/components/ui/button"; import { useMemo, useState } from "react"; -import { Plus, RefreshCw } from "lucide-react"; +import { ExternalLink, Plus, RefreshCw } from "lucide-react"; import { Card, CardContent, @@ -107,56 +107,25 @@ export function DNSRecordsDataTable({
-
+
-

DNS Records

+

DNS Records

Required
- {tabs && tabs.length > 0 && ( - - - {tabs.map((tab) => ( - - {tab.label} ( - {data.filter(tab.filterFn).length}) - - ))} - - - )} -
-
- {onRefresh && ( - - )} - {onAdd && addButtonText && ( - - )} +
{table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => ( {header.isPlaceholder diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx index 0f967f33..df63e1df 100644 --- a/src/components/DomainInfoCard.tsx +++ b/src/components/DomainInfoCard.tsx @@ -155,7 +155,7 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) <> - + {t("type")} From 7370448be99d85ab66664dd6ff5b06155c3de17a Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sun, 19 Oct 2025 23:13:27 +0530 Subject: [PATCH 18/38] pg schema --- server/db/pg/schema/schema.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index ae5205bb..0b1192f7 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -24,6 +24,17 @@ export const domains = pgTable("domains", { preferWildcardCert: boolean("preferWildcardCert") }); + +export const dnsRecords = pgTable("dnsRecords", { + id: varchar("id").primaryKey(), + domainId: varchar("domainId") + .notNull() + .references(() => domains.domainId, { onDelete: "cascade" }), + recordType: varchar("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT" + baseDomain: varchar("baseDomain"), + value: varchar("value").notNull(), +}); + export const orgs = pgTable("orgs", { orgId: varchar("orgId").primaryKey(), name: varchar("name").notNull(), From edf64ae7b5090d1e4704d0c5396c55ff5ca667c6 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Sun, 19 Oct 2025 23:23:55 +0530 Subject: [PATCH 19/38] fix invalid "default" --- src/app/[orgId]/settings/domains/[domainId]/layout.tsx | 4 ++-- src/app/[orgId]/settings/domains/[domainId]/page.tsx | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx index c75ceaf4..7c2c3ca5 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx @@ -8,11 +8,11 @@ import SettingsLayoutClient from "./DomainSettingsLayout"; interface SettingsLayoutProps { children: React.ReactNode; - params: Promise<{ domainId: string; orgId: string }>; + params: { domainId: string; orgId: string }; } export default async function SettingsLayout({ children, params }: SettingsLayoutProps) { - const { domainId, orgId } = await params; + const { domainId, orgId } = params; let domain = null; try { diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx index 791ba96d..02efa517 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/page.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx @@ -1,5 +1,3 @@ - - -export default function DomainPage({ children }: { children: React.ReactNode }) { - return <>{children}; -} \ No newline at end of file +export default function DomainPage() { + return null; +} From 2b05bc1f5f228382c4c9fec74d19f81f71697a20 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Mon, 20 Oct 2025 01:39:39 +0530 Subject: [PATCH 20/38] ui and layout fix --- messages/en-US.json | 4 +- server/db/pg/schema/schema.ts | 1 + server/db/sqlite/schema/schema.ts | 1 + .../[domainId]/DomainSettingsLayout.tsx | 112 ------------------ .../settings/domains/[domainId]/layout.tsx | 20 ++-- .../settings/domains/[domainId]/page.tsx | 108 ++++++++++++++++- src/components/DNSRecordTable.tsx | 8 +- src/components/DNSRecordsDataTable.tsx | 8 +- src/components/DomainInfoCard.tsx | 33 ++++-- src/contexts/domainContext.ts | 12 +- src/providers/DomainProvider.tsx | 6 +- 11 files changed, 159 insertions(+), 154 deletions(-) delete mode 100644 src/app/[orgId]/settings/domains/[domainId]/DomainSettingsLayout.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 4d382869..d53765cf 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1904,5 +1904,7 @@ "recordName": "Record Name", "auto": "Auto", "TTL": "TTL", - "howToAddRecords": "How to Add Records" + "howToAddRecords": "How to Add Records", + "dnsRecord": "DNS Records", + "required": "Required" } diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 0b1192f7..36130d36 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -33,6 +33,7 @@ export const dnsRecords = pgTable("dnsRecords", { recordType: varchar("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT" baseDomain: varchar("baseDomain"), value: varchar("value").notNull(), + verified: boolean("verified").notNull().default(false), }); export const orgs = pgTable("orgs", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index bc1ce81b..d3390c21 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -25,6 +25,7 @@ export const dnsRecords = sqliteTable("dnsRecords", { recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT" baseDomain: text("baseDomain"), value: text("value").notNull(), + verified: integer("verified", { mode: "boolean" }).notNull().default(false), }); diff --git a/src/app/[orgId]/settings/domains/[domainId]/DomainSettingsLayout.tsx b/src/app/[orgId]/settings/domains/[domainId]/DomainSettingsLayout.tsx deleted file mode 100644 index 3b822dc0..00000000 --- a/src/app/[orgId]/settings/domains/[domainId]/DomainSettingsLayout.tsx +++ /dev/null @@ -1,112 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; -import { useRouter } from "next/navigation"; -import { RefreshCw } from "lucide-react"; -import { Button } from "@app/components/ui/button"; -import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import DomainInfoCard from "@app/components/DomainInfoCard"; -import DomainProvider from "@app/providers/DomainProvider"; -import { useTranslations } from "next-intl"; - - -interface DomainSettingsLayoutProps { - orgId: string; - domain: any, - children: React.ReactNode; -} - -export default function DomainSettingsLayout({ orgId, domain, children }: DomainSettingsLayoutProps) { - const router = useRouter(); - const api = createApiClient(useEnvContext()); - const [isRefreshing, setIsRefreshing] = useState(false); - const [restartingDomains, setRestartingDomains] = useState>(new Set()); - const t = useTranslations(); - 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; - }); - } - }; - - const isRestarting = restartingDomains.has(domain.domainId); - - return ( - <> -
- - - -
- - -
- -
- {children} -
- - ); -} diff --git a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx index 7c2c3ca5..d33d666a 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/layout.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/layout.tsx @@ -3,18 +3,17 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { internal } from "@app/lib/api"; import { GetDomainResponse } from "@server/routers/domain/getDomain"; import { AxiosResponse } from "axios"; -import { getTranslations } from "next-intl/server"; -import SettingsLayoutClient from "./DomainSettingsLayout"; +import DomainProvider from "@app/providers/DomainProvider"; interface SettingsLayoutProps { children: React.ReactNode; - params: { domainId: string; orgId: string }; + params: Promise<{ domainId: string; orgId: string }>; } export default async function SettingsLayout({ children, params }: SettingsLayoutProps) { - const { domainId, orgId } = params; - + const { domainId, orgId } = await params; let domain = null; + try { const res = await internal.get>( `/org/${orgId}/domain/${domainId}`, @@ -25,14 +24,9 @@ export default async function SettingsLayout({ children, params }: SettingsLayou redirect(`/${orgId}/settings/domains`); } - const t = await getTranslations(); - return ( - + {children} - + ); -} +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx index 02efa517..3051def0 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/page.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx @@ -1,3 +1,105 @@ -export default function DomainPage() { - return null; -} +"use client"; +import { useState } from "react"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { useRouter } from "next/navigation"; +import { RefreshCw } from "lucide-react"; +import { Button } from "@app/components/ui/button"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import DomainInfoCard from "@app/components/DomainInfoCard"; +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 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 isRestarting = restartingDomains.has(domain.domainId); + + return ( + <> +
+ + +
+
+ +
+ + ); +} \ No newline at end of file diff --git a/src/components/DNSRecordTable.tsx b/src/components/DNSRecordTable.tsx index e0760977..393f5c24 100644 --- a/src/components/DNSRecordTable.tsx +++ b/src/components/DNSRecordTable.tsx @@ -1,12 +1,8 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { Button } from "@app/components/ui/button"; -import { useState } from "react"; import { useTranslations } from "next-intl"; -import { useToast } from "@app/hooks/useToast"; import { Badge } from "@app/components/ui/badge"; -import CopyToClipboard from "@app/components/CopyToClipboard"; import { DNSRecordsDataTable } from "./DNSRecordsDataTable"; export type DNSRecordRow = { @@ -114,7 +110,9 @@ export default function DNSRecordsTable({ records, domainId, isRefreshing }: Pro verified ? ( {t("verified")} ) : ( - {t("unverified")} + + {t("failed", { fallback: "Failed" })} + ) ); } diff --git a/src/components/DNSRecordsDataTable.tsx b/src/components/DNSRecordsDataTable.tsx index 422cf318..b29c67f8 100644 --- a/src/components/DNSRecordsDataTable.tsx +++ b/src/components/DNSRecordsDataTable.tsx @@ -108,9 +108,9 @@ export function DNSRecordsDataTable({
-
-

DNS Records

- Required +
+

{t("dnsRecord")}

+ {t("required")}
{table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => ( {header.isPlaceholder diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx index df63e1df..a53a2420 100644 --- a/src/components/DomainInfoCard.tsx +++ b/src/components/DomainInfoCard.tsx @@ -30,7 +30,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; import { Switch } from "./ui/switch"; import { useEffect, useState } from "react"; -import DNSRecordsTable, {DNSRecordRow} from "./DNSRecordTable"; +import DNSRecordsTable, { DNSRecordRow } from "./DNSRecordTable"; import { createApiClient } from "@app/lib/api"; import { useToast } from "@app/hooks/useToast"; import { formatAxiosError } from "@app/lib/api"; @@ -150,19 +150,33 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) } }, [domain.domainId]); + const getTypeDisplay = (type: string) => { + switch (type) { + case "ns": + return t("selectDomainTypeNsName"); + case "cname": + return t("selectDomainTypeCnameName"); + case "wildcard": + return t("selectDomainTypeWildcardName"); + default: + return type; + } + }; + + return ( <> - + {t("type")} - {domain.type} + {getTypeDisplay(domain.type ? domain.type : "")} @@ -172,16 +186,11 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) {domain.verified ? ( -
- - {t("verified")} - -
+ {t("verified")} ) : ( -
-
- {t("unverified")} -
+ + {t("failed", { fallback: "Failed" })} + )}
diff --git a/src/contexts/domainContext.ts b/src/contexts/domainContext.ts index d60c4ca4..e38ddefb 100644 --- a/src/contexts/domainContext.ts +++ b/src/contexts/domainContext.ts @@ -1,11 +1,19 @@ import { GetDomainResponse } from "@server/routers/domain/getDomain"; -import { createContext } from "react"; - +import { createContext, useContext } from "react"; interface DomainContextType { domain: GetDomainResponse; updateDomain: (updatedDomain: Partial) => void; + orgId: string; } const DomainContext = createContext(undefined); +export function useDomain() { + const context = useContext(DomainContext); + if (!context) { + throw new Error("useDomain must be used within DomainProvider"); + } + return context; +} + export default DomainContext; \ No newline at end of file diff --git a/src/providers/DomainProvider.tsx b/src/providers/DomainProvider.tsx index 9b014449..845b369f 100644 --- a/src/providers/DomainProvider.tsx +++ b/src/providers/DomainProvider.tsx @@ -8,11 +8,13 @@ import DomainContext from "@app/contexts/domainContext"; interface DomainProviderProps { children: React.ReactNode; domain: GetDomainResponse; + orgId: string; } export function DomainProvider({ children, - domain: serverDomain + domain: serverDomain, + orgId }: DomainProviderProps) { const [domain, setDomain] = useState(serverDomain); @@ -34,7 +36,7 @@ export function DomainProvider({ }; return ( - + {children} ); From 07f5e8f21590922706975e2096a3ffb17281a2ac Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Mon, 20 Oct 2025 17:29:23 +0530 Subject: [PATCH 21/38] add update domain Settings for wildcard --- messages/en-US.json | 5 +- server/auth/actions.ts | 1 + server/routers/domain/index.ts | 3 +- server/routers/domain/updateDomain.ts | 161 ++++++++++++++ server/routers/external.ts | 7 + src/components/DNSRecordsDataTable.tsx | 4 +- src/components/DomainInfoCard.tsx | 291 +++++++++++++++---------- 7 files changed, 352 insertions(+), 120 deletions(-) create mode 100644 server/routers/domain/updateDomain.ts diff --git a/messages/en-US.json b/messages/en-US.json index d53765cf..c4990aae 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1906,5 +1906,8 @@ "TTL": "TTL", "howToAddRecords": "How to Add Records", "dnsRecord": "DNS Records", - "required": "Required" + "required": "Required", + "domainSettingsUpdated": "Domain settings updated successfully", + "orgOrDomainIdMissing": "Organization or Domain ID is missing", + "loadingDNSRecords": "Loading DNS records..." } diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 4c442d2c..83582885 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -82,6 +82,7 @@ export enum ActionsEnum { getClient = "getClient", listOrgDomains = "listOrgDomains", getDomain = "getDomain", + updateOrgDomain = "updateOrgDomain", getDNSRecords = "getDNSRecords", createNewt = "createNewt", createIdp = "createIdp", diff --git a/server/routers/domain/index.ts b/server/routers/domain/index.ts index 0bfedb41..e7e0b555 100644 --- a/server/routers/domain/index.ts +++ b/server/routers/domain/index.ts @@ -3,4 +3,5 @@ export * from "./createOrgDomain"; export * from "./deleteOrgDomain"; export * from "./restartOrgDomain"; export * from "./getDomain"; -export * from "./getDNSRecords"; \ No newline at end of file +export * from "./getDNSRecords"; +export * from "./updateDomain"; \ No newline at end of file diff --git a/server/routers/domain/updateDomain.ts b/server/routers/domain/updateDomain.ts new file mode 100644 index 00000000..c684466e --- /dev/null +++ b/server/routers/domain/updateDomain.ts @@ -0,0 +1,161 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, domains, orgDomains } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { eq, and } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const paramsSchema = z + .object({ + orgId: z.string(), + domainId: z.string() + }) + .strict(); + +const bodySchema = z + .object({ + certResolver: z.string().optional().nullable(), + preferWildcardCert: z.boolean().optional().nullable() + }) + .strict(); + +export type UpdateDomainResponse = { + domainId: string; + certResolver: string | null; + preferWildcardCert: boolean | null; +}; + + +registry.registerPath({ + method: "patch", + path: "/org/{orgId}/domain/{domainId}", + description: "Update a domain by domainId.", + tags: [OpenAPITags.Domain], + request: { + params: z.object({ + domainId: z.string(), + orgId: z.string() + }) + }, + responses: {} +}); + +export async function updateOrgDomain( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { orgId, domainId } = parsedParams.data; + const { certResolver, preferWildcardCert } = parsedBody.data; + + const [orgDomain] = await db + .select() + .from(orgDomains) + .where( + and( + eq(orgDomains.orgId, orgId), + eq(orgDomains.domainId, domainId) + ) + ); + + if (!orgDomain) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Domain not found or does not belong to this organization" + ) + ); + } + + + const [existingDomain] = await db + .select() + .from(domains) + .where(eq(domains.domainId, domainId)); + + if (!existingDomain) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Domain not found") + ); + } + + if (existingDomain.type !== "wildcard") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Domain settings can only be updated for wildcard domains" + ) + ); + } + + const updateData: Partial<{ + certResolver: string | null; + preferWildcardCert: boolean; + }> = {}; + + if (certResolver !== undefined) { + updateData.certResolver = certResolver; + } + + if (preferWildcardCert !== undefined && preferWildcardCert !== null) { + updateData.preferWildcardCert = preferWildcardCert; + } + + const [updatedDomain] = await db + .update(domains) + .set(updateData) + .where(eq(domains.domainId, domainId)) + .returning(); + + if (!updatedDomain) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to update domain" + ) + ); + } + + return response(res, { + data: { + domainId: updatedDomain.domainId, + certResolver: updatedDomain.certResolver, + preferWildcardCert: updatedDomain.preferWildcardCert + }, + success: true, + error: false, + message: "Domain updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index c00f1e9f..fcc39ded 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -309,6 +309,13 @@ authenticated.get( domain.getDomain ); +authenticated.patch( + "/org/:orgId/domain/:domainId", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateOrgDomain), + domain.updateOrgDomain +) + authenticated.get( "/org/:orgId/domain/:domainId/dns-records", verifyOrgAccess, diff --git a/src/components/DNSRecordsDataTable.tsx b/src/components/DNSRecordsDataTable.tsx index b29c67f8..672012ae 100644 --- a/src/components/DNSRecordsDataTable.tsx +++ b/src/components/DNSRecordsDataTable.tsx @@ -125,7 +125,7 @@ export function DNSRecordsDataTable({
{table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => ( {header.isPlaceholder @@ -165,7 +165,7 @@ export function DNSRecordsDataTable({ colSpan={columns.length} className="h-24 text-center" > - No results found. + {t("noResults")} )} diff --git a/src/components/DomainInfoCard.tsx b/src/components/DomainInfoCard.tsx index a53a2420..2465c608 100644 --- a/src/components/DomainInfoCard.tsx +++ b/src/components/DomainInfoCard.tsx @@ -105,6 +105,7 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) const [dnsRecords, setDnsRecords] = useState([]); const [loadingRecords, setLoadingRecords] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); + const [saveLoading, setSaveLoading] = useState(false); const form = useForm({ resolver: zodResolver(formSchema), @@ -116,6 +117,21 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) } }); + useEffect(() => { + if (domain.domainId) { + const certResolverValue = domain.certResolver && domain.certResolver.trim() !== "" + ? domain.certResolver + : null; + + form.reset({ + baseDomain: domain.baseDomain || "", + type: (domain.type as "ns" | "cname" | "wildcard") || "wildcard", + certResolver: certResolverValue, + preferWildcardCert: domain.preferWildcardCert || false + }); + } + }, [domain]); + const fetchDNSRecords = async (showRefreshing = false) => { if (showRefreshing) { setIsRefreshing(true); @@ -150,6 +166,49 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) } }, [domain.domainId]); + const onSubmit = async (values: FormValues) => { + if (!orgId || !domainId) { + toast({ + title: t("error"), + description: t("orgOrDomainIdMissing", { fallback: "Organization or Domain ID is missing" }), + variant: "destructive" + }); + return; + } + + setSaveLoading(true); + + try { + const response = await api.patch( + `/org/${orgId}/domain/${domainId}`, + { + certResolver: values.certResolver, + preferWildcardCert: values.preferWildcardCert + } + ); + + updateDomain({ + ...domain, + certResolver: values.certResolver || null, + preferWildcardCert: values.preferWildcardCert || false + }); + + toast({ + title: t("success"), + description: t("domainSettingsUpdated", { fallback: "Domain settings updated successfully" }), + variant: "default" + }); + } catch (error) { + toast({ + title: t("error"), + description: formatAxiosError(error), + variant: "destructive" + }); + } finally { + setSaveLoading(false); + } + }; + const getTypeDisplay = (type: string) => { switch (type) { case "ns": @@ -198,128 +257,128 @@ export default function DomainInfoCard({ orgId, domainId }: DomainInfoCardProps) - {loadingRecords ? ( -
- loading... -
- ) : ( - + {domain.type !== "wildcard" && ( + loadingRecords ? ( +
+ {t("loadingDNSRecords", { fallback: "Loading DNS Records..." })} +
+ ) : ( + + ) )} - {/* Domain Settings */} - {/* Add condition later to only show when domain is wildcard */} - - - - - {t("domainSetting")} - - + {/* Domain Settings - Only show for wildcard domains */} + {domain.type === "wildcard" && ( + + + + + {t("domainSetting")} + + - - -
- - ( - - {t("certResolver")} - - - - - - - {certResolverOptions.map((opt) => ( - - {opt.title} - - ))} - - - - - {field.value !== null && field.value !== "default" && ( -
- - field.onChange(e.target.value)} + onValueChange={(val) => { + if (val === "default") { + field.onChange(null); + } else if (val === "custom") { + field.onChange(""); + } else { + field.onChange(val); + } + }} + > + + + + + {certResolverOptions.map((opt) => ( + + {opt.title} + + ))} + + + + + {field.value !== null && field.value !== "default" && ( +
+ + field.onChange(e.target.value)} + /> + + ( + + +
+ + {t("preferWildcardCert")} +
+
+ + + {t("preferWildcardCertDescription")} + + +
+ )} /> - - ( - - -
- - {t("preferWildcardCert")} -
-
+
+ )} + + )} + /> + + + + - - {t("preferWildcardCertDescription")} - - - - )} - /> -
- )} -
- )} - /> - - -
-
- - - - -
-
+ + + +
+
+ )} ); } \ No newline at end of file From 7a6838f5a50d29a676d06e18c3d56f614a1b8073 Mon Sep 17 00:00:00 2001 From: Pallavi Kumari Date: Mon, 20 Oct 2025 20:29:35 +0530 Subject: [PATCH 22/38] fix lint --- server/routers/external.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/external.ts b/server/routers/external.ts index fcc39ded..f862e5cf 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -314,7 +314,7 @@ authenticated.patch( verifyOrgAccess, verifyUserHasAction(ActionsEnum.updateOrgDomain), domain.updateOrgDomain -) +); authenticated.get( "/org/:orgId/domain/:domainId/dns-records", From 70aeaf7b5d425959fb351af21d64c885f9d7a1eb Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 20 Oct 2025 20:11:13 -0700 Subject: [PATCH 23/38] Change badges and button size --- src/app/[orgId]/settings/domains/[domainId]/page.tsx | 1 - src/components/DNSRecordTable.tsx | 4 ++-- src/components/DNSRecordsDataTable.tsx | 1 - src/components/DomainInfoCard.tsx | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx index 3051def0..c7e137f6 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/page.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx @@ -76,7 +76,6 @@ export default function DomainSettingsPage() { /> + ); + }, + cell: ({ row }) => { + const originalRow = row.original; + + return ( +
+ +
+ Olm + {originalRow.olmVersion && ( + + v{originalRow.olmVersion} + + )} +
+
+ {originalRow.olmUpdateAvailable && ( + + )} +
+ ); + } + }, { accessorKey: "subnet", header: ({ column }) => { @@ -282,7 +325,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) { {t("deleteClientQuestion")}

- {t("clientMessageRemove")} + {t("clientMessageRemove")}

} From 4e4a38f7e9f8e6a886bd8d3555e48f1760eb21d4 Mon Sep 17 00:00:00 2001 From: Lokowitz Date: Thu, 23 Oct 2025 13:19:27 +0000 Subject: [PATCH 29/38] move to match type country instead of geoip --- server/setup/migrationsPg.ts | 4 +++- server/setup/migrationsSqlite.ts | 4 +++- server/setup/scriptsPg/1.11.2.ts | 24 +++++++++++++++++++ server/setup/scriptsSqlite/1.11.2.ts | 18 ++++++++++++++ .../resources/[niceId]/rules/page.tsx | 20 ++++++++-------- 5 files changed, 58 insertions(+), 12 deletions(-) create mode 100644 server/setup/scriptsPg/1.11.2.ts create mode 100644 server/setup/scriptsSqlite/1.11.2.ts diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index c8e632e0..b6d20512 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -13,6 +13,7 @@ import m5 from "./scriptsPg/1.10.0"; import m6 from "./scriptsPg/1.10.2"; import m7 from "./scriptsPg/1.11.0"; import m8 from "./scriptsPg/1.11.1"; +import m9 from "./scriptsPg/1.11.2"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -26,7 +27,8 @@ const migrations = [ { version: "1.10.0", run: m5 }, { version: "1.10.2", run: m6 }, { version: "1.11.0", run: m7 }, - { version: "1.11.1", run: m8 } + { version: "1.11.1", run: m8 }, + { version: "1.11.2", run: m9 } // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index e65d7436..d60db7a0 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -31,6 +31,7 @@ import m26 from "./scriptsSqlite/1.10.1"; import m27 from "./scriptsSqlite/1.10.2"; import m28 from "./scriptsSqlite/1.11.0"; import m29 from "./scriptsSqlite/1.11.1"; +import m30 from "./scriptsSqlite/1.11.2"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -60,7 +61,8 @@ const migrations = [ { version: "1.10.1", run: m26 }, { version: "1.10.2", run: m27 }, { version: "1.11.0", run: m28 }, - { version: "1.11.1", run: m29 } + { version: "1.11.1", run: m29 }, + { version: "1.11.2", run: m30 } // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsPg/1.11.2.ts b/server/setup/scriptsPg/1.11.2.ts new file mode 100644 index 00000000..6f61e727 --- /dev/null +++ b/server/setup/scriptsPg/1.11.2.ts @@ -0,0 +1,24 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; + +const version = "1.11.2"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + await db.execute(sql`BEGIN`); + + await db.execute(sql`UPDATE "resourceRules" SET "match" = "COUNTRY" WHERE "match" = "GEOIP"`); + + await db.execute(sql`COMMIT`); + console.log(`Updated resource rules match value from GEOIP to COUNTRY`); + } catch (e) { + await db.execute(sql`ROLLBACK`); + console.log("Unable to update resource rules match value"); + console.log(e); + throw e; + } + + console.log(`${version} migration complete`); +} diff --git a/server/setup/scriptsSqlite/1.11.2.ts b/server/setup/scriptsSqlite/1.11.2.ts new file mode 100644 index 00000000..dfc1b7ae --- /dev/null +++ b/server/setup/scriptsSqlite/1.11.2.ts @@ -0,0 +1,18 @@ +import { APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import path from "path"; + +const version = "1.11.2"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + db.transaction(() => { + db.prepare(`UPDATE resourceRules SET match = "COUNTRY" WHERE match = "GEOIP"`).run(); + })(); + + console.log(`${version} migration complete`); +} diff --git a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx index 1cf08c82..dada372f 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx @@ -130,7 +130,7 @@ export default function ResourceRules(props: { PATH: t('path'), IP: "IP", CIDR: t('ipAddressRange'), - GEOIP: t('country') + COUNTRY: t('country') } as const; const addRuleForm = useForm({ @@ -212,7 +212,7 @@ export default function ResourceRules(props: { setLoading(false); return; } - if (data.match === "GEOIP" && !COUNTRIES.some(c => c.code === data.value)) { + if (data.match === "COUNTRY" && !COUNTRIES.some(c => c.code === data.value)) { toast({ variant: "destructive", title: t('rulesErrorInvalidCountry'), @@ -270,7 +270,7 @@ export default function ResourceRules(props: { return t('rulesMatchIpAddress'); case "PATH": return t('rulesMatchUrl'); - case "GEOIP": + case "COUNTRY": return t('rulesMatchCountry'); } } @@ -492,8 +492,8 @@ export default function ResourceRules(props: { cell: ({ row }) => ( @@ -514,7 +514,7 @@ export default function ResourceRules(props: { accessorKey: "value", header: t('value'), cell: ({ row }) => ( - row.original.match === "GEOIP" ? ( + row.original.match === "COUNTRY" ? (