🚧 Create certificate for Redirect (in case of domain)

This commit is contained in:
Fred KISSIE
2026-09-14 23:42:20 +02:00
parent 4a41e6b650
commit fd7780528f
4 changed files with 94 additions and 39 deletions
@@ -459,6 +459,11 @@ export async function getTraefikConfig(
} }
}; };
console.dir(
{ resourcesMap, resourcesWithTargetsAndSites },
{ depth: null }
);
// get the key and the resource // get the key and the resource
for (const [, resource] of resourcesMap.entries()) { for (const [, resource] of resourcesMap.entries()) {
const targets = resource.targets as TargetWithSite[]; const targets = resource.targets as TargetWithSite[];
+48 -30
View File
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, domains, orgDomains, redirects, resources } from "@server/db"; 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 response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
@@ -17,6 +17,7 @@ import {
redirectRewritePathTypeSchema redirectRewritePathTypeSchema
} from "@server/routers/redirect/validation"; } from "@server/routers/redirect/validation";
import { getUniqueRedirectName } from "@server/db/names"; import { getUniqueRedirectName } from "@server/db/names";
import { createCertificate } from "../certificates";
export type CreateRedirectResponse = { export type CreateRedirectResponse = {
redirect: Redirect; redirect: Redirect;
@@ -26,31 +27,37 @@ const paramsSchema = z.strictObject({
orgId: z.string().nonempty() orgId: z.string().nonempty()
}); });
const bodySchema = z.strictObject({ const bodySchema = z
name: z.string().nonempty(), .strictObject({
resourceId: z.number().int().positive().optional().nullable(), name: z.string().nonempty(),
domainId: z.string().nonempty().optional().nullable(), resourceId: z.number().int().positive().optional().nullable(),
subdomain: z.string().nonempty().optional().nullable(), domainId: z.string().nonempty().optional().nullable(),
destinationDomain: redirectDestinationDomainSchema, subdomain: z.string().nonempty().optional().nullable(),
pathMatchType: redirectPathMatchTypeSchema.optional(), destinationDomain: redirectDestinationDomainSchema,
matchPath: redirectMatchPathSchema, pathMatchType: redirectPathMatchTypeSchema.optional(),
rewritePath: redirectRewritePathSchema.optional().nullable(), matchPath: redirectMatchPathSchema,
rewritePathType: redirectRewritePathTypeSchema.optional().nullable(), rewritePath: redirectRewritePathSchema.optional().nullable(),
permanent: z.boolean().optional(), rewritePathType: redirectRewritePathTypeSchema.optional().nullable(),
enabled: z.boolean().optional() permanent: z.boolean().optional(),
}).refine( enabled: z.boolean().optional()
(data) => })
// stripPrefix removes the matched prefix and needs no replacement .refine(
// value; every other rewrite type is meaningless without one. (data) =>
!data.rewritePathType || // stripPrefix removes the matched prefix and needs no replacement
data.rewritePathType === "stripPrefix" || // value; every other rewrite type is meaningless without one.
Boolean(data.rewritePath), !data.rewritePathType ||
{ data.rewritePathType === "stripPrefix" ||
message: Boolean(data.rewritePath),
"rewritePath is required unless rewritePathType is stripPrefix", {
path: ["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({ registry.registerPath({
method: "put", method: "put",
@@ -115,9 +122,10 @@ export async function createRedirect(
enabled enabled
} = parsedBody.data; } = parsedBody.data;
let resource: Resource | null = null;
if (resourceId) { if (resourceId) {
const [resource] = await db const res = await db
.select({ resourceId: resources.resourceId }) .select()
.from(resources) .from(resources)
.where( .where(
and( and(
@@ -127,6 +135,7 @@ export async function createRedirect(
) )
.limit(1); .limit(1);
resource = res.at(0) ?? null;
if (!resource) { if (!resource) {
return next( return next(
createHttpError( createHttpError(
@@ -137,9 +146,10 @@ export async function createRedirect(
} }
} }
let domain: Domain | null = null;
if (domainId) { if (domainId) {
const [domain] = await db const res = await db
.select({ domainId: domains.domainId }) .select()
.from(domains) .from(domains)
.innerJoin( .innerJoin(
orgDomains, orgDomains,
@@ -153,6 +163,7 @@ export async function createRedirect(
) )
.limit(1); .limit(1);
domain = res.at(0)?.domains ?? null;
if (!domain) { if (!domain) {
return next( return next(
createHttpError( createHttpError(
@@ -184,6 +195,13 @@ export async function createRedirect(
}) })
.returning(); .returning();
if (domain) {
const fullDomain = [subdomain ?? null, domain.baseDomain]
.filter(Boolean)
.join(".");
await createCertificate(domain.domainId, fullDomain, db);
}
return response<CreateRedirectResponse>(res, { return response<CreateRedirectResponse>(res, {
data: { data: {
redirect redirect
+38 -5
View File
@@ -17,6 +17,7 @@ import {
redirectRewritePathSchema, redirectRewritePathSchema,
redirectRewritePathTypeSchema redirectRewritePathTypeSchema
} from "@server/routers/redirect/validation"; } from "@server/routers/redirect/validation";
import { createCertificate } from "../certificates";
export type UpdateRedirectResponse = { export type UpdateRedirectResponse = {
redirect: Redirect; 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) { if (body.resourceId) {
const [resource] = await db const [resource] = await db
.select({ resourceId: resources.resourceId }) .select({ resourceId: resources.resourceId })
@@ -135,9 +152,13 @@ export async function updateRedirect(
} }
} }
if (body.domainId) { let domain: { domainId: string; baseDomain: string } | null = null;
const [domain] = await db if (effectiveDomainId) {
.select({ domainId: domains.domainId }) const [d] = await db
.select({
domainId: domains.domainId,
baseDomain: domains.baseDomain
})
.from(domains) .from(domains)
.innerJoin( .innerJoin(
orgDomains, orgDomains,
@@ -145,17 +166,18 @@ export async function updateRedirect(
) )
.where( .where(
and( and(
eq(domains.domainId, body.domainId), eq(domains.domainId, effectiveDomainId),
eq(orgDomains.orgId, existing.orgId) eq(orgDomains.orgId, existing.orgId)
) )
) )
.limit(1); .limit(1);
domain = d ?? null;
if (!domain) { if (!domain) {
return next( return next(
createHttpError( createHttpError(
HttpCode.NOT_FOUND, 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(); .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<UpdateRedirectResponse>(res, { return response<UpdateRedirectResponse>(res, {
data: { data: {
redirect redirect
@@ -29,8 +29,8 @@ export async function traefikConfigProvider(
const traefikConfig = await getTraefikConfig( const traefikConfig = await getTraefikConfig(
currentExitNodeId, currentExitNodeId,
config.getRawConfig().traefik.site_types, config.getRawConfig().traefik.site_types,
build == "oss", // filter out the namespace domains in open source build === "oss", // filter out the namespace domains in open source
build != "oss", // generate the login pages on the cloud and and enterprise, build !== "oss", // generate the login pages on the cloud and and enterprise,
config.getRawConfig().traefik.allow_raw_resources, config.getRawConfig().traefik.allow_raw_resources,
pangolinUIUrl, pangolinUIUrl,
pangolinUIUrl, pangolinUIUrl,
@@ -71,8 +71,7 @@ export async function traefikConfigProvider(
.resource_session_request_param, .resource_session_request_param,
remoteUserIdHeader: remoteUserIdHeader:
config.getRawConfig().server.remote_headers config.getRawConfig().server.remote_headers.user_id,
.user_id,
remoteVirtualApiKeyIdHeader: remoteVirtualApiKeyIdHeader:
config.getRawConfig().server.remote_headers config.getRawConfig().server.remote_headers