From e128b8c282fffbc2cbe89fc09bff71d92ea23576 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 24 Jul 2026 19:07:37 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20get=20batched=20certificates=20endp?= =?UTF-8?q?oint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../certificates/getBatchedCertificates.ts | 116 ++++++++++++------ server/private/routers/certificates/index.ts | 1 + server/private/routers/external.ts | 7 ++ server/routers/certificates/types.ts | 11 +- 4 files changed, 98 insertions(+), 37 deletions(-) diff --git a/server/private/routers/certificates/getBatchedCertificates.ts b/server/private/routers/certificates/getBatchedCertificates.ts index 6962b9c53..ba51b77b5 100644 --- a/server/private/routers/certificates/getBatchedCertificates.ts +++ b/server/private/routers/certificates/getBatchedCertificates.ts @@ -12,7 +12,7 @@ */ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { certificates, db, domains } from "@server/db"; +import { certificates, db, domains, orgDomains } from "@server/db"; import { eq, and, or, like, inArray } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -20,9 +20,16 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { registry } from "@server/openApi"; -import { GetCertificateResponse } from "@server/routers/certificates/types"; +import { + GetCertificateResponse, + type GetBatchedCertificateResponse +} from "@server/routers/certificates/types"; -const getCertificateSchema = z.strictObject({ +const getCertificateParamSchema = z.strictObject({ + orgId: z.string() +}); + +const getCertificateQuerySchema = z.object({ domains: z.preprocess( (val) => { if (val === undefined || val === null || val === "") { @@ -38,11 +45,10 @@ const getCertificateSchema = z.strictObject({ return undefined; }, z.array(z.string().min(1).max(255)) - ), - orgId: z.string() + ) }); -async function query(domainId: string, domain: string) { +async function query1(domainId: string, domain: string) { const [domainRecord] = await db .select() .from(domains) @@ -116,7 +122,7 @@ async function query(domainId: string, domain: string) { return existing.length > 0 ? { ...existing[0], domainType } : null; } -async function query2(domainList: string[]) { +async function query(orgId: string, domainList: string[]) { // Try to get CNAME certificates first let existingCertificates = await db .select({ @@ -129,31 +135,39 @@ async function query2(domainList: string[]) { createdAt: certificates.createdAt, updatedAt: certificates.updatedAt, errorMessage: certificates.errorMessage, - renewalCount: certificates.renewalCount + renewalCount: certificates.renewalCount, + domainType: domains.type }) .from(certificates) .innerJoin(domains, eq(certificates.domainId, domains.domainId)) + .innerJoin( + orgDomains, + and( + eq(domains.domainId, orgDomains.domainId), + eq(orgDomains.orgId, orgId) + ) + ) .where(and(inArray(certificates.domain, domainList))); - // All non resolved domain certificates might be `ns` or `wildcard` + // All non resolved domain certificates might be `ns` or `wildcard`, + // which means exact domain certificates do not const nonAvailableCertificates = existingCertificates .filter((cert) => !domainList.includes(cert.domain)) .map((cert) => cert.domain); if (nonAvailableCertificates.length > 0) { - const wildcardPrefixedDomains = nonAvailableCertificates.map( - (domain) => { - const domainLevelDown = domain.split(".").slice(1).join("."); - const wildcardPrefixed = `*.${domainLevelDown}`; - return { - domainLevelDown, - wildcardPrefixed - }; - } - ); + const domainLevelDownSet = new Set(); + const wildcardDomainSet = new Set(); + + for (const domain of nonAvailableCertificates) { + const domainLevelDown = domain.split(".").slice(1).join("."); + const wildcardPrefixed = `*.${domainLevelDown}`; + domainLevelDownSet.add(domainLevelDown); + wildcardDomainSet.add(wildcardPrefixed); + } // Need to map the certificates to each domain - await db + const wildcardCertificates = await db .select({ certId: certificates.certId, domain: certificates.domain, @@ -164,33 +178,54 @@ async function query2(domainList: string[]) { createdAt: certificates.createdAt, updatedAt: certificates.updatedAt, errorMessage: certificates.errorMessage, - renewalCount: certificates.renewalCount + renewalCount: certificates.renewalCount, + domainType: domains.type }) .from(certificates) + .innerJoin(domains, eq(certificates.domainId, domains.domainId)) + .innerJoin( + orgDomains, + and( + eq(domains.domainId, orgDomains.domainId), + eq(orgDomains.orgId, orgId) + ) + ) .where( - or( - eq(certificates.domain, domain), - and( - eq(certificates.wildcard, true), - or( - eq(certificates.domain, domainLevelDown), - eq(certificates.domain, wildcardPrefixed) - ) + and( + eq(certificates.wildcard, true), + or( + inArray(certificates.domain, [...domainLevelDownSet]), + inArray(certificates.domain, [...wildcardDomainSet]) ) ) ); + + existingCertificates.push(...wildcardCertificates); } - return; + const certificateMap: Record = {}; + for (const domain of domainList) { + const domainLevelDown = domain.split(".").slice(1).join("."); + const wildcardPrefixed = `*.${domainLevelDown}`; + certificateMap[domain] = + existingCertificates.find( + (cert) => + cert.domain === domain || + cert.domain === domainLevelDown || + cert.domain === wildcardPrefixed + ) ?? null; + } + + return certificateMap; } -export async function getBatchedCertificate( +export async function getBatchedCertificates( req: Request, res: Response, next: NextFunction ): Promise { try { - const parsedParams = getCertificateSchema.safeParse(req.params); + const parsedParams = getCertificateParamSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( @@ -200,11 +235,22 @@ export async function getBatchedCertificate( ); } - const { domains } = parsedParams.data; + const { orgId } = parsedParams.data; + const parsedQuery = getCertificateQuerySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } - const cert = await query(domains); + const { domains } = parsedQuery.data; - return response(res, { + const cert = await query(orgId, domains); + + return response(res, { data: cert, success: true, error: false, diff --git a/server/private/routers/certificates/index.ts b/server/private/routers/certificates/index.ts index 18b942d5c..54b11aa1e 100644 --- a/server/private/routers/certificates/index.ts +++ b/server/private/routers/certificates/index.ts @@ -14,3 +14,4 @@ export * from "./getCertificate"; export * from "./restartCertificate"; export * from "./syncCertToNewts"; +export * from "./getBatchedCertificates"; diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index d2dc7bec1..fab026418 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -175,6 +175,13 @@ authenticated.get( certificates.getCertificate ); +authenticated.get( + "/org/:orgId/batched-certificates", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getCertificate), + certificates.getBatchedCertificates +); + authenticated.post( "/org/:orgId/certificate/:certId/restart", verifyValidLicense, diff --git a/server/routers/certificates/types.ts b/server/routers/certificates/types.ts index e6aeecdf8..55fe92183 100644 --- a/server/routers/certificates/types.ts +++ b/server/routers/certificates/types.ts @@ -1,9 +1,11 @@ +import type { Domain } from "@server/db"; + export type GetCertificateResponse = { certId: number; domain: string; domainId: string; wildcard: boolean; - domainType: string; + domainType: Domain["type"]; status: string; // pending, requested, valid, expired, failed expiresAt: string | null; lastRenewalAttempt: Date | null; @@ -11,4 +13,9 @@ export type GetCertificateResponse = { updatedAt: number; errorMessage?: string | null; renewalCount: number; -}; \ No newline at end of file +}; + +export type GetBatchedCertificateResponse = Record< + string, + GetCertificateResponse | null +>;