From 2e60ecec87e673d0404d0456436c56ba6bd6d11b Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 22 Dec 2025 14:00:34 -0500 Subject: [PATCH] Add maintence options to blueprints --- server/lib/blueprints/proxyResources.ts | 112 +++++++++++++++++------- server/lib/blueprints/types.ts | 4 +- server/private/lib/blueprints/types.ts | 9 ++ 3 files changed, 93 insertions(+), 32 deletions(-) create mode 100644 server/private/lib/blueprints/types.ts diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index 6488dd32..a8f2f4b5 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -2,7 +2,8 @@ import { domains, orgDomains, Resource, - resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility, + resourceHeaderAuth, + resourceHeaderAuthExtendedCompatibility, resourcePincode, resourceRules, resourceWhitelist, @@ -16,8 +17,8 @@ import { userResources, users } from "@server/db"; -import {resources, targets, sites} from "@server/db"; -import {eq, and, asc, or, ne, count, isNotNull} from "drizzle-orm"; +import { resources, targets, sites } from "@server/db"; +import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm"; import { Config, ConfigSchema, @@ -25,12 +26,13 @@ import { TargetData } from "./types"; import logger from "@server/logger"; -import {createCertificate} from "#dynamic/routers/certificates/createCertificate"; -import {pickPort} from "@server/routers/target/helpers"; -import {resourcePassword} from "@server/db"; -import {hashPassword} from "@server/auth/password"; -import {isValidCIDR, isValidIP, isValidUrlGlobPattern} from "../validators"; -import {get} from "http"; +import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; +import { pickPort } from "@server/routers/target/helpers"; +import { resourcePassword } from "@server/db"; +import { hashPassword } from "@server/auth/password"; +import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; +import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed"; +import { build } from "@server/build"; export type ProxyResourcesResults = { proxyResource: Resource; @@ -63,7 +65,7 @@ export async function updateProxyResources( if (targetSiteId) { // Look up site by niceId [site] = await trx - .select({siteId: sites.siteId}) + .select({ siteId: sites.siteId }) .from(sites) .where( and( @@ -75,7 +77,7 @@ export async function updateProxyResources( } else if (siteId) { // Use the provided siteId directly, but verify it belongs to the org [site] = await trx - .select({siteId: sites.siteId}) + .select({ siteId: sites.siteId }) .from(sites) .where( and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)) @@ -93,7 +95,7 @@ export async function updateProxyResources( let internalPortToCreate; if (!targetData["internal-port"]) { - const {internalPort, targetIps} = await pickPort( + const { internalPort, targetIps } = await pickPort( site.siteId!, trx ); @@ -209,6 +211,16 @@ export async function updateProxyResources( resource = existingResource; } else { // Update existing resource + + const isLicensed = await isLicensedOrSubscribed(orgId); + if (build == "enterprise" && !isLicensed) { + logger.warn( + "Server is not licensed! Clearing set maintenance screen values" + ); + // null the maintenance mode fields if not licensed + resourceData.maintenance = undefined; + } + [resource] = await trx .update(resources) .set({ @@ -228,12 +240,19 @@ export async function updateProxyResources( tlsServerName: resourceData["tls-server-name"] || null, emailWhitelistEnabled: resourceData.auth?.[ "whitelist-users" - ] + ] ? resourceData.auth["whitelist-users"].length > 0 : false, headers: headers || null, applyRules: - resourceData.rules && resourceData.rules.length > 0 + resourceData.rules && resourceData.rules.length > 0, + maintenanceModeEnabled: + resourceData.maintenance?.enabled, + maintenanceModeType: resourceData.maintenance?.type, + maintenanceTitle: resourceData.maintenance?.title, + maintenanceMessage: resourceData.maintenance?.message, + maintenanceEstimatedTime: + resourceData.maintenance?.["estimated-time"] }) .where( eq(resources.resourceId, existingResource.resourceId) @@ -303,8 +322,13 @@ export async function updateProxyResources( const headerAuthPassword = resourceData.auth?.["basic-auth"]?.password; const headerAuthExtendedCompatibility = - resourceData.auth?.["basic-auth"]?.extendedCompatibility; - if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) { + resourceData.auth?.["basic-auth"] + ?.extendedCompatibility; + if ( + headerAuthUser && + headerAuthPassword && + headerAuthExtendedCompatibility !== null + ) { const headerAuthHash = await hashPassword( Buffer.from( `${headerAuthUser}:${headerAuthPassword}` @@ -315,10 +339,13 @@ export async function updateProxyResources( resourceId: existingResource.resourceId, headerAuthHash }), - trx.insert(resourceHeaderAuthExtendedCompatibility).values({ - resourceId: existingResource.resourceId, - extendedCompatibilityIsActivated: headerAuthExtendedCompatibility - }) + trx + .insert(resourceHeaderAuthExtendedCompatibility) + .values({ + resourceId: existingResource.resourceId, + extendedCompatibilityIsActivated: + headerAuthExtendedCompatibility + }) ]); } } @@ -380,7 +407,7 @@ export async function updateProxyResources( if (targetSiteId) { // Look up site by niceId [site] = await trx - .select({siteId: sites.siteId}) + .select({ siteId: sites.siteId }) .from(sites) .where( and( @@ -392,7 +419,7 @@ export async function updateProxyResources( } else if (siteId) { // Use the provided siteId directly, but verify it belongs to the org [site] = await trx - .select({siteId: sites.siteId}) + .select({ siteId: sites.siteId }) .from(sites) .where( and( @@ -437,7 +464,7 @@ export async function updateProxyResources( if (checkIfTargetChanged(existingTarget, updatedTarget)) { let internalPortToUpdate; if (!targetData["internal-port"]) { - const {internalPort, targetIps} = await pickPort( + const { internalPort, targetIps } = await pickPort( site.siteId!, trx ); @@ -622,6 +649,15 @@ export async function updateProxyResources( ); } + const isLicensed = await isLicensedOrSubscribed(orgId); + if (build == "enterprise" && !isLicensed) { + logger.warn( + "Server is not licensed! Clearing set maintenance screen values" + ); + // null the maintenance mode fields if not licensed + resourceData.maintenance = undefined; + } + // Create new resource const [newResource] = await trx .insert(resources) @@ -643,7 +679,13 @@ export async function updateProxyResources( ssl: resourceSsl, headers: headers || null, applyRules: - resourceData.rules && resourceData.rules.length > 0 + resourceData.rules && resourceData.rules.length > 0, + maintenanceModeEnabled: resourceData.maintenance?.enabled, + maintenanceModeType: resourceData.maintenance?.type, + maintenanceTitle: resourceData.maintenance?.title, + maintenanceMessage: resourceData.maintenance?.message, + maintenanceEstimatedTime: + resourceData.maintenance?.["estimated-time"] }) .returning(); @@ -674,9 +716,14 @@ export async function updateProxyResources( const headerAuthUser = resourceData.auth?.["basic-auth"]?.user; const headerAuthPassword = resourceData.auth?.["basic-auth"]?.password; - const headerAuthExtendedCompatibility = resourceData.auth?.["basic-auth"]?.extendedCompatibility; + const headerAuthExtendedCompatibility = + resourceData.auth?.["basic-auth"]?.extendedCompatibility; - if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) { + if ( + headerAuthUser && + headerAuthPassword && + headerAuthExtendedCompatibility !== null + ) { const headerAuthHash = await hashPassword( Buffer.from( `${headerAuthUser}:${headerAuthPassword}` @@ -688,10 +735,13 @@ export async function updateProxyResources( resourceId: newResource.resourceId, headerAuthHash }), - trx.insert(resourceHeaderAuthExtendedCompatibility).values({ - resourceId: newResource.resourceId, - extendedCompatibilityIsActivated: headerAuthExtendedCompatibility - }), + trx + .insert(resourceHeaderAuthExtendedCompatibility) + .values({ + resourceId: newResource.resourceId, + extendedCompatibilityIsActivated: + headerAuthExtendedCompatibility + }) ]); } } @@ -1043,7 +1093,7 @@ async function getDomain( trx: Transaction ) { const [fullDomainExists] = await trx - .select({resourceId: resources.resourceId}) + .select({ resourceId: resources.resourceId }) .from(resources) .where( and( diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 00bd33af..cfc71ac2 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { portRangeStringSchema } from "@server/lib/ip"; +import { MaintenanceSchema } from "#dynamic/lib/blueprints/types"; export const SiteSchema = z.object({ name: z.string().min(1).max(100), @@ -156,7 +157,8 @@ export const ResourceSchema = z "host-header": z.string().optional(), "tls-server-name": z.string().optional(), headers: z.array(HeaderSchema).optional(), - rules: z.array(RuleSchema).optional() + rules: z.array(RuleSchema).optional(), + maintenance: MaintenanceSchema.optional() }) .refine( (resource) => { diff --git a/server/private/lib/blueprints/types.ts b/server/private/lib/blueprints/types.ts new file mode 100644 index 00000000..31663de3 --- /dev/null +++ b/server/private/lib/blueprints/types.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const MaintenanceSchema = z.object({ + enabled: z.boolean().optional(), + type: z.enum(["forced", "automatic"]).optional(), + title: z.string().max(255).nullable().optional(), + message: z.string().max(2000).nullable().optional(), + "estimated-time": z.string().max(100).nullable().optional() +});