add cert resolver

This commit is contained in:
Pallavi Kumari
2025-10-06 21:00:07 +05:30
parent 60379a7b4e
commit 3f3e9cf1bb
5 changed files with 92 additions and 22 deletions

View File

@@ -1891,5 +1891,9 @@
"cannotbeUndone": "This can not be undone.", "cannotbeUndone": "This can not be undone.",
"toConfirm": "to confirm", "toConfirm": "to confirm",
"deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?", "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"
} }

View File

@@ -11,7 +11,9 @@ export const domains = sqliteTable("domains", {
type: text("type"), // "ns", "cname", "wildcard" type: text("type"), // "ns", "cname", "wildcard"
verified: integer("verified", { mode: "boolean" }).notNull().default(false), verified: integer("verified", { mode: "boolean" }).notNull().default(false),
failed: integer("failed", { 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", { export const orgs = sqliteTable("orgs", {

View File

@@ -24,16 +24,21 @@ const paramsSchema = z
const bodySchema = z const bodySchema = z
.object({ .object({
type: z.enum(["ns", "cname", "wildcard"]), 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(); .strict();
export type CreateDomainResponse = { export type CreateDomainResponse = {
domainId: string; domainId: string;
nsRecords?: string[]; nsRecords?: string[];
cnameRecords?: { baseDomain: string; value: string }[]; cnameRecords?: { baseDomain: string; value: string }[];
aRecords?: { baseDomain: string; value: string }[]; aRecords?: { baseDomain: string; value: string }[];
txtRecords?: { 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 // 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 { orgId } = parsedParams.data;
const { type, baseDomain } = parsedBody.data; const { type, baseDomain, certResolver, customCertResolver } = parsedBody.data;
if (build == "oss") { if (build == "oss") {
if (type !== "wildcard") { if (type !== "wildcard") {
@@ -254,7 +259,9 @@ export async function createOrgDomain(
domainId, domainId,
baseDomain, baseDomain,
type, type,
verified: type === "wildcard" ? true : false verified: type === "wildcard" ? true : false,
certResolver: certResolver || null,
customCertResolver: customCertResolver || null
}) })
.returning(); .returning();
@@ -325,7 +332,9 @@ export async function createOrgDomain(
cnameRecords, cnameRecords,
txtRecords, txtRecords,
nsRecords, nsRecords,
aRecords aRecords,
certResolver: returned.certResolver,
customCertResolver: returned.customCertResolver
}, },
success: true, success: true,
error: false, error: false,

View File

@@ -42,7 +42,9 @@ async function queryDomains(orgId: string, limit: number, offset: number) {
type: domains.type, type: domains.type,
failed: domains.failed, failed: domains.failed,
tries: domains.tries, tries: domains.tries,
configManaged: domains.configManaged configManaged: domains.configManaged,
certResolver: domains.certResolver,
customCertResolver: domains.customCertResolver,
}) })
.from(orgDomains) .from(orgDomains)
.where(eq(orgDomains.orgId, orgId)) .where(eq(orgDomains.orgId, orgId))

View File

@@ -45,6 +45,7 @@ import {
import { useOrgContext } from "@app/hooks/useOrgContext"; import { useOrgContext } from "@app/hooks/useOrgContext";
import { build } from "@server/build"; import { build } from "@server/build";
import { toASCII, toUnicode } from 'punycode'; import { toASCII, toUnicode } from 'punycode';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
// Helper functions for Unicode domain handling // Helper functions for Unicode domain handling
@@ -96,7 +97,9 @@ const formSchema = z.object({
.min(1, "Domain is required") .min(1, "Domain is required")
.refine((val) => isValidDomainFormat(val), "Invalid domain format") .refine((val) => isValidDomainFormat(val), "Invalid domain format")
.transform((val) => toPunycode(val)), .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<typeof formSchema>; type FormValues = z.infer<typeof formSchema>;
@@ -107,6 +110,12 @@ type CreateDomainFormProps = {
onCreated?: (domain: CreateDomainResponse) => void; 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({ export default function CreateDomainForm({
open, open,
setOpen, setOpen,
@@ -125,15 +134,26 @@ export default function CreateDomainForm({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
baseDomain: "", 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(); form.reset();
setLoading(false); setLoading(false);
setCreatedDomain(null); setCreatedDomain(null);
} };
async function onSubmit(values: FormValues) { async function onSubmit(values: FormValues) {
setLoading(true); setLoading(true);
@@ -158,17 +178,9 @@ export default function CreateDomainForm({
} finally { } finally {
setLoading(false); 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 = []; let domainOptions: any = [];
if (build != "oss" && env.flags.usePangolinDns) { if (build != "oss" && env.flags.usePangolinDns) {
domainOptions = [ domainOptions = [
@@ -250,7 +262,7 @@ export default function CreateDomainForm({
<AlertTitle>{t("internationaldomaindetected")}</AlertTitle> <AlertTitle>{t("internationaldomaindetected")}</AlertTitle>
<AlertDescription> <AlertDescription>
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
<p>{t("willbestoredas")} <code className="font-mono px-1 py-0.5 rounded">{punycodePreview}</code></p> <p>{t("willbestoredas")} <code className="font-mono px-1 py-0.5 rounded">{punycodePreview}</code></p>
</div> </div>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@@ -260,6 +272,47 @@ export default function CreateDomainForm({
</FormItem> </FormItem>
)} )}
/> />
{domainType === "wildcard" && (
<FormField
control={form.control}
name="certResolver"
render={({ field }) => (
<FormItem>
<FormLabel>{t("certResolver")}</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(val) => field.onChange(val)}
>
<SelectTrigger>
<SelectValue placeholder={t("selectCertResolver")} />
</SelectTrigger>
<SelectContent>
{certResolverOptions.map((opt) => (
<SelectItem key={opt.id} value={opt.id}>
{opt.title}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
{field.value === "custom" && (
<FormControl className="mt-2">
<Input
placeholder={t("enterCustomResolver")}
value={form.watch("customCertResolver") || ""}
onChange={(e) =>
form.setValue("customCertResolver", e.target.value)
}
/>
</FormControl>
)}
</FormItem>
)}
/>
)}
</form> </form>
</Form> </Form>
) : ( ) : (