mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-10 20:02:26 +00:00
Merge branch 'feat/cert-resolver-through-UI' of github.com:Pallavikumarimdb/pangolin into Pallavikumarimdb-feat/cert-resolver-through-UI
This commit is contained in:
@@ -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";
|
||||
@@ -24,16 +24,21 @@ const paramsSchema = z
|
||||
const bodySchema = z
|
||||
.object({
|
||||
type: z.enum(["ns", "cname", "wildcard"]),
|
||||
baseDomain: subdomainSchema
|
||||
baseDomain: subdomainSchema,
|
||||
certResolver: z.string().optional().nullable(),
|
||||
preferWildcardCert: z.boolean().optional().nullable() // optional, only for wildcard
|
||||
})
|
||||
.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;
|
||||
preferWildcardCert?: boolean | 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, preferWildcardCert } = 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,
|
||||
preferWildcardCert: preferWildcardCert || false
|
||||
})
|
||||
.returning();
|
||||
|
||||
@@ -269,9 +276,24 @@ 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,
|
||||
verified: false
|
||||
});
|
||||
}
|
||||
} else if (type === "cname") {
|
||||
cnameRecords = [
|
||||
{
|
||||
@@ -283,6 +305,18 @@ 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,
|
||||
verified: false
|
||||
});
|
||||
}
|
||||
} else if (type === "wildcard") {
|
||||
aRecords = [
|
||||
{
|
||||
@@ -294,6 +328,23 @@ 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,
|
||||
verified: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Insert all DNS records in batch
|
||||
if (recordsToInsert.length > 0) {
|
||||
await trx.insert(dnsRecords).values(recordsToInsert);
|
||||
}
|
||||
|
||||
numOrgDomains = await trx
|
||||
@@ -325,7 +376,9 @@ export async function createOrgDomain(
|
||||
cnameRecords,
|
||||
txtRecords,
|
||||
nsRecords,
|
||||
aRecords
|
||||
aRecords,
|
||||
certResolver: returned.certResolver,
|
||||
preferWildcardCert: returned.preferWildcardCert
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
97
server/routers/domain/getDNSRecords.ts
Normal file
97
server/routers/domain/getDNSRecords.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
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";
|
||||
import { getServerIp } from "@server/lib/serverIpService"; // your in-memory IP module
|
||||
|
||||
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<ReturnType<typeof query>>;
|
||||
|
||||
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<any> {
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const serverIp = getServerIp();
|
||||
|
||||
// Override value for type A or wildcard records
|
||||
const updatedRecords = records.map(record => {
|
||||
if ((record.recordType === "A" || record.baseDomain === "*") && serverIp) {
|
||||
return { ...record, value: serverIp };
|
||||
}
|
||||
return record;
|
||||
});
|
||||
|
||||
return response<GetDNSRecordsResponse>(res, {
|
||||
data: updatedRecords,
|
||||
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")
|
||||
);
|
||||
}
|
||||
}
|
||||
86
server/routers/domain/getDomain.ts
Normal file
86
server/routers/domain/getDomain.ts
Normal file
@@ -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<Awaited<ReturnType<typeof query>>>;
|
||||
|
||||
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<any> {
|
||||
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<GetDomainResponse>(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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
export * from "./listDomains";
|
||||
export * from "./createOrgDomain";
|
||||
export * from "./deleteOrgDomain";
|
||||
export * from "./restartOrgDomain";
|
||||
export * from "./restartOrgDomain";
|
||||
export * from "./getDomain";
|
||||
export * from "./getDNSRecords";
|
||||
export * from "./updateDomain";
|
||||
@@ -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,
|
||||
preferWildcardCert: domains.preferWildcardCert
|
||||
})
|
||||
.from(orgDomains)
|
||||
.where(eq(orgDomains.orgId, orgId))
|
||||
|
||||
161
server/routers/domain/updateDomain.ts
Normal file
161
server/routers/domain/updateDomain.ts
Normal file
@@ -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<any> {
|
||||
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<UpdateDomainResponse>(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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -302,6 +302,27 @@ authenticated.get(
|
||||
domain.listDomains
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/domain/:domainId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.getDomain),
|
||||
domain.getDomain
|
||||
);
|
||||
|
||||
authenticated.patch(
|
||||
"/org/:orgId/domain/:domainId",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.updateOrgDomain),
|
||||
domain.updateOrgDomain
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/domain/:domainId/dns-records",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.getDNSRecords),
|
||||
domain.getDNSRecords
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/invitations",
|
||||
verifyOrgAccess,
|
||||
|
||||
Reference in New Issue
Block a user