From fd7780528f78c8343ae9f6d1365820dadf5c3bef Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 14 Sep 2026 23:42:20 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20Create=20certificate=20for=20Red?= =?UTF-8?q?irect=20=20(in=20case=20of=20domain)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../private/lib/traefik/getTraefikConfig.ts | 5 ++ server/routers/redirect/createRedirect.ts | 78 ++++++++++++------- server/routers/redirect/updateRedirect.ts | 43 ++++++++-- .../routers/traefik/traefikConfigProvider.ts | 7 +- 4 files changed, 94 insertions(+), 39 deletions(-) diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 0515b8265..39f7ade06 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -459,6 +459,11 @@ export async function getTraefikConfig( } }; + console.dir( + { resourcesMap, resourcesWithTargetsAndSites }, + { depth: null } + ); + // get the key and the resource for (const [, resource] of resourcesMap.entries()) { const targets = resource.targets as TargetWithSite[]; diff --git a/server/routers/redirect/createRedirect.ts b/server/routers/redirect/createRedirect.ts index d1ca209a3..97303066f 100644 --- a/server/routers/redirect/createRedirect.ts +++ b/server/routers/redirect/createRedirect.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, domains, orgDomains, redirects, resources } from "@server/db"; -import type { Redirect } from "@server/db"; +import type { Domain, Redirect, Resource } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -17,6 +17,7 @@ import { redirectRewritePathTypeSchema } from "@server/routers/redirect/validation"; import { getUniqueRedirectName } from "@server/db/names"; +import { createCertificate } from "../certificates"; export type CreateRedirectResponse = { redirect: Redirect; @@ -26,31 +27,37 @@ const paramsSchema = z.strictObject({ orgId: z.string().nonempty() }); -const bodySchema = z.strictObject({ - name: z.string().nonempty(), - resourceId: z.number().int().positive().optional().nullable(), - domainId: z.string().nonempty().optional().nullable(), - subdomain: z.string().nonempty().optional().nullable(), - destinationDomain: redirectDestinationDomainSchema, - pathMatchType: redirectPathMatchTypeSchema.optional(), - matchPath: redirectMatchPathSchema, - rewritePath: redirectRewritePathSchema.optional().nullable(), - rewritePathType: redirectRewritePathTypeSchema.optional().nullable(), - permanent: z.boolean().optional(), - enabled: z.boolean().optional() -}).refine( - (data) => - // stripPrefix removes the matched prefix and needs no replacement - // value; every other rewrite type is meaningless without one. - !data.rewritePathType || - data.rewritePathType === "stripPrefix" || - Boolean(data.rewritePath), - { - message: - "rewritePath is required unless rewritePathType is stripPrefix", - path: ["rewritePath"] - } -); +const bodySchema = z + .strictObject({ + name: z.string().nonempty(), + resourceId: z.number().int().positive().optional().nullable(), + domainId: z.string().nonempty().optional().nullable(), + subdomain: z.string().nonempty().optional().nullable(), + destinationDomain: redirectDestinationDomainSchema, + pathMatchType: redirectPathMatchTypeSchema.optional(), + matchPath: redirectMatchPathSchema, + rewritePath: redirectRewritePathSchema.optional().nullable(), + rewritePathType: redirectRewritePathTypeSchema.optional().nullable(), + permanent: z.boolean().optional(), + enabled: z.boolean().optional() + }) + .refine( + (data) => + // stripPrefix removes the matched prefix and needs no replacement + // value; every other rewrite type is meaningless without one. + !data.rewritePathType || + data.rewritePathType === "stripPrefix" || + Boolean(data.rewritePath), + { + message: + "rewritePath is required unless rewritePathType is stripPrefix", + path: ["rewritePath"] + } + ) + .refine((data) => Boolean(data.resourceId) !== Boolean(data.domainId), { + message: "Exactly one of resourceId or domainId must be provided", + path: ["resourceId"] + }); registry.registerPath({ method: "put", @@ -115,9 +122,10 @@ export async function createRedirect( enabled } = parsedBody.data; + let resource: Resource | null = null; if (resourceId) { - const [resource] = await db - .select({ resourceId: resources.resourceId }) + const res = await db + .select() .from(resources) .where( and( @@ -127,6 +135,7 @@ export async function createRedirect( ) .limit(1); + resource = res.at(0) ?? null; if (!resource) { return next( createHttpError( @@ -137,9 +146,10 @@ export async function createRedirect( } } + let domain: Domain | null = null; if (domainId) { - const [domain] = await db - .select({ domainId: domains.domainId }) + const res = await db + .select() .from(domains) .innerJoin( orgDomains, @@ -153,6 +163,7 @@ export async function createRedirect( ) .limit(1); + domain = res.at(0)?.domains ?? null; if (!domain) { return next( createHttpError( @@ -184,6 +195,13 @@ export async function createRedirect( }) .returning(); + if (domain) { + const fullDomain = [subdomain ?? null, domain.baseDomain] + .filter(Boolean) + .join("."); + await createCertificate(domain.domainId, fullDomain, db); + } + return response(res, { data: { redirect diff --git a/server/routers/redirect/updateRedirect.ts b/server/routers/redirect/updateRedirect.ts index 0a6b53a3c..59b87f59c 100644 --- a/server/routers/redirect/updateRedirect.ts +++ b/server/routers/redirect/updateRedirect.ts @@ -17,6 +17,7 @@ import { redirectRewritePathSchema, redirectRewritePathTypeSchema } from "@server/routers/redirect/validation"; +import { createCertificate } from "../certificates"; export type UpdateRedirectResponse = { redirect: Redirect; @@ -113,6 +114,22 @@ export async function updateRedirect( ); } + const effectiveResourceId = + body.resourceId !== undefined + ? body.resourceId + : existing.resourceId; + const effectiveDomainId = + body.domainId !== undefined ? body.domainId : existing.domainId; + + if (Boolean(effectiveResourceId) === Boolean(effectiveDomainId)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Exactly one of resourceId or domainId must be provided" + ) + ); + } + if (body.resourceId) { const [resource] = await db .select({ resourceId: resources.resourceId }) @@ -135,9 +152,13 @@ export async function updateRedirect( } } - if (body.domainId) { - const [domain] = await db - .select({ domainId: domains.domainId }) + let domain: { domainId: string; baseDomain: string } | null = null; + if (effectiveDomainId) { + const [d] = await db + .select({ + domainId: domains.domainId, + baseDomain: domains.baseDomain + }) .from(domains) .innerJoin( orgDomains, @@ -145,17 +166,18 @@ export async function updateRedirect( ) .where( and( - eq(domains.domainId, body.domainId), + eq(domains.domainId, effectiveDomainId), eq(orgDomains.orgId, existing.orgId) ) ) .limit(1); + domain = d ?? null; if (!domain) { return next( createHttpError( HttpCode.NOT_FOUND, - `Domain with ID ${body.domainId} not found` + `Domain with ID ${effectiveDomainId} not found` ) ); } @@ -234,6 +256,17 @@ export async function updateRedirect( ) .returning(); + if (domain) { + const effectiveSubdomain = + body.subdomain !== undefined + ? body.subdomain + : existing.subdomain; + const fullDomain = [effectiveSubdomain ?? null, domain.baseDomain] + .filter(Boolean) + .join("."); + await createCertificate(domain.domainId, fullDomain, db); + } + return response(res, { data: { redirect diff --git a/server/routers/traefik/traefikConfigProvider.ts b/server/routers/traefik/traefikConfigProvider.ts index 02e05f5e0..3c0f9e73c 100644 --- a/server/routers/traefik/traefikConfigProvider.ts +++ b/server/routers/traefik/traefikConfigProvider.ts @@ -29,8 +29,8 @@ export async function traefikConfigProvider( const traefikConfig = await getTraefikConfig( currentExitNodeId, config.getRawConfig().traefik.site_types, - build == "oss", // filter out the namespace domains in open source - build != "oss", // generate the login pages on the cloud and and enterprise, + build === "oss", // filter out the namespace domains in open source + build !== "oss", // generate the login pages on the cloud and and enterprise, config.getRawConfig().traefik.allow_raw_resources, pangolinUIUrl, pangolinUIUrl, @@ -71,8 +71,7 @@ export async function traefikConfigProvider( .resource_session_request_param, remoteUserIdHeader: - config.getRawConfig().server.remote_headers - .user_id, + config.getRawConfig().server.remote_headers.user_id, remoteVirtualApiKeyIdHeader: config.getRawConfig().server.remote_headers