Add flag for generate own certs

This commit is contained in:
Owen
2025-10-13 10:32:41 -07:00
parent 5917881b47
commit e7828a43fa
13 changed files with 362 additions and 303 deletions

View File

@@ -8,9 +8,7 @@ import { db, exitNodes } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { getCurrentExitNodeId } from "@server/lib/exitNodes"; import { getCurrentExitNodeId } from "@server/lib/exitNodes";
import { getTraefikConfig } from "#dynamic/lib/traefik"; import { getTraefikConfig } from "#dynamic/lib/traefik";
import { import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
getValidCertificatesForDomains,
} from "#dynamic/lib/certificates";
import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { build } from "@server/build"; import { build } from "@server/build";
@@ -311,84 +309,92 @@ export class TraefikConfigManager {
this.lastActiveDomains = new Set(domains); this.lastActiveDomains = new Set(domains);
} }
// Scan current local certificate state if (
this.lastLocalCertificateState = process.env.GENERATE_OWN_CERTIFICATES === "true" &&
await this.scanLocalCertificateState(); build != "oss"
) {
// Scan current local certificate state
this.lastLocalCertificateState =
await this.scanLocalCertificateState();
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check) // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
let validCertificates: Array<{ let validCertificates: Array<{
id: number; id: number;
domain: string; domain: string;
wildcard: boolean | null; wildcard: boolean | null;
certFile: string | null; certFile: string | null;
keyFile: string | null; keyFile: string | null;
expiresAt: number | null; expiresAt: number | null;
updatedAt?: number | null; updatedAt?: number | null;
}> = []; }> = [];
if (this.shouldFetchCertificates(domains)) { if (this.shouldFetchCertificates(domains)) {
// Filter out domains that are already covered by wildcard certificates // Filter out domains that are already covered by wildcard certificates
const domainsToFetch = new Set<string>(); const domainsToFetch = new Set<string>();
for (const domain of domains) { for (const domain of domains) {
if ( if (
!isDomainCoveredByWildcard( !isDomainCoveredByWildcard(
domain, domain,
this.lastLocalCertificateState this.lastLocalCertificateState
) )
) { ) {
domainsToFetch.add(domain); domainsToFetch.add(domain);
} else { } else {
logger.debug( logger.debug(
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch` `Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
); );
}
} }
}
if (domainsToFetch.size > 0) { if (domainsToFetch.size > 0) {
// Get valid certificates for domains not covered by wildcards // Get valid certificates for domains not covered by wildcards
validCertificates = validCertificates =
await getValidCertificatesForDomains(domainsToFetch); await getValidCertificatesForDomains(
this.lastCertificateFetch = new Date(); domainsToFetch
this.lastKnownDomains = new Set(domains); );
this.lastCertificateFetch = new Date();
this.lastKnownDomains = new Set(domains);
logger.info( logger.info(
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)` `Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
); );
// Download and decrypt new certificates // Download and decrypt new certificates
await this.processValidCertificates(validCertificates); await this.processValidCertificates(validCertificates);
} else {
logger.info(
"All domains are covered by existing wildcard certificates, no fetch needed"
);
this.lastCertificateFetch = new Date();
this.lastKnownDomains = new Set(domains);
}
// Always ensure all existing certificates (including wildcards) are in the config
await this.updateDynamicConfigFromLocalCerts(domains);
} else { } else {
logger.info( const timeSinceLastFetch = this.lastCertificateFetch
"All domains are covered by existing wildcard certificates, no fetch needed" ? Math.round(
); (Date.now() -
this.lastCertificateFetch = new Date(); this.lastCertificateFetch.getTime()) /
this.lastKnownDomains = new Set(domains); (1000 * 60)
)
: 0;
// logger.debug(
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
// );
// Still need to ensure config is up to date with existing certificates
await this.updateDynamicConfigFromLocalCerts(domains);
} }
// Always ensure all existing certificates (including wildcards) are in the config // Clean up certificates for domains no longer in use
await this.updateDynamicConfigFromLocalCerts(domains); await this.cleanupUnusedCertificates(domains);
} else {
const timeSinceLastFetch = this.lastCertificateFetch
? Math.round(
(Date.now() - this.lastCertificateFetch.getTime()) /
(1000 * 60)
)
: 0;
// logger.debug( // wait 1 second for traefik to pick up the new certificates
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)` await new Promise((resolve) => setTimeout(resolve, 500));
// );
// Still need to ensure config is up to date with existing certificates
await this.updateDynamicConfigFromLocalCerts(domains);
} }
// Clean up certificates for domains no longer in use
await this.cleanupUnusedCertificates(domains);
// wait 1 second for traefik to pick up the new certificates
await new Promise((resolve) => setTimeout(resolve, 500));
// Write traefik config as YAML to a second dynamic config file if changed // Write traefik config as YAML to a second dynamic config file if changed
await this.writeTraefikDynamicConfig(traefikConfig); await this.writeTraefikDynamicConfig(traefikConfig);
@@ -690,7 +696,12 @@ export class TraefikConfigManager {
for (const cert of validCertificates) { for (const cert of validCertificates) {
try { try {
if (!cert.certFile || !cert.keyFile) { if (
!cert.certFile ||
!cert.keyFile ||
cert.certFile.length === 0 ||
cert.keyFile.length === 0
) {
logger.warn( logger.warn(
`Certificate for domain ${cert.domain} is missing cert or key file` `Certificate for domain ${cert.domain} is missing cert or key file`
); );

View File

@@ -105,7 +105,12 @@ export async function getTraefikConfig(
const priority = row.priority ?? 100; const priority = row.priority ?? 100;
// Create a unique key combining resourceId, path config, and rewrite config // Create a unique key combining resourceId, path config, and rewrite config
const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType] const pathKey = [
targetPath,
pathMatchType,
rewritePath,
rewritePathType
]
.filter(Boolean) .filter(Boolean)
.join("-"); .join("-");
const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
@@ -120,13 +125,15 @@ export async function getTraefikConfig(
); );
if (!validation.isValid) { if (!validation.isValid) {
logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`); logger.error(
`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`
);
return; return;
} }
resourcesMap.set(key, { resourcesMap.set(key, {
resourceId: row.resourceId, resourceId: row.resourceId,
name: resourceName, name: resourceName,
fullDomain: row.fullDomain, fullDomain: row.fullDomain,
ssl: row.ssl, ssl: row.ssl,
http: row.http, http: row.http,
@@ -239,21 +246,18 @@ export async function getTraefikConfig(
preferWildcardCert = configDomain.prefer_wildcard_cert; preferWildcardCert = configDomain.prefer_wildcard_cert;
} }
let tls = {}; const tls = {
if (build == "oss") { certResolver: certResolver,
tls = { ...(preferWildcardCert
certResolver: certResolver, ? {
...(preferWildcardCert domains: [
? { {
domains: [ main: wildCard
{ }
main: wildCard ]
} }
] : {})
} };
: {})
};
}
const additionalMiddlewares = const additionalMiddlewares =
config.getRawConfig().traefik.additional_middlewares || []; config.getRawConfig().traefik.additional_middlewares || [];
@@ -264,11 +268,12 @@ export async function getTraefikConfig(
]; ];
// Handle path rewriting middleware // Handle path rewriting middleware
if (resource.rewritePath && if (
resource.rewritePath &&
resource.path && resource.path &&
resource.pathMatchType && resource.pathMatchType &&
resource.rewritePathType) { resource.rewritePathType
) {
// Create a unique middleware name // Create a unique middleware name
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`; const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
@@ -287,7 +292,10 @@ export async function getTraefikConfig(
} }
// the middleware to the config // the middleware to the config
Object.assign(config_output.http.middlewares, rewriteResult.middlewares); Object.assign(
config_output.http.middlewares,
rewriteResult.middlewares
);
// middlewares to the router middleware chain // middlewares to the router middleware chain
if (rewriteResult.chain) { if (rewriteResult.chain) {
@@ -298,9 +306,13 @@ export async function getTraefikConfig(
routerMiddlewares.push(rewriteMiddlewareName); routerMiddlewares.push(rewriteMiddlewareName);
} }
logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`); logger.debug(
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
);
} catch (error) { } catch (error) {
logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`); logger.error(
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
);
} }
} }
@@ -316,7 +328,9 @@ export async function getTraefikConfig(
value: string; value: string;
}[]; }[];
} catch (e) { } catch (e) {
logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`); logger.warn(
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
);
} }
headersArr.forEach((header) => { headersArr.forEach((header) => {
@@ -482,14 +496,14 @@ export async function getTraefikConfig(
})(), })(),
...(resource.stickySession ...(resource.stickySession
? { ? {
sticky: { sticky: {
cookie: { cookie: {
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
secure: resource.ssl, secure: resource.ssl,
httpOnly: true httpOnly: true
} }
} }
} }
: {}) : {})
} }
}; };
@@ -590,13 +604,13 @@ export async function getTraefikConfig(
})(), })(),
...(resource.stickySession ...(resource.stickySession
? { ? {
sticky: { sticky: {
ipStrategy: { ipStrategy: {
depth: 0, depth: 0,
sourcePort: true sourcePort: true
} }
} }
} }
: {}) : {})
} }
}; };

View File

@@ -148,6 +148,10 @@ export class PrivateConfig {
if (parsedPrivateConfig.stripe?.s3Region) { if (parsedPrivateConfig.stripe?.s3Region) {
process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region; process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region;
} }
if (parsedPrivateConfig.flags?.generate_own_certificates) {
process.env.GENERATE_OWN_CERTIFICATES =
parsedPrivateConfig.flags.generate_own_certificates.toString();
}
} }
this.rawPrivateConfig = parsedPrivateConfig; this.rawPrivateConfig = parsedPrivateConfig;

View File

@@ -17,7 +17,7 @@ import { MemoryStore, Store } from "express-rate-limit";
import RedisStore from "#private/lib/redisStore"; import RedisStore from "#private/lib/redisStore";
export function createStore(): Store { export function createStore(): Store {
if (build != "oss" && privateConfig.getRawPrivateConfig().flags?.enable_redis) { if (build != "oss" && privateConfig.getRawPrivateConfig().flags.enable_redis) {
const rateLimitStore: Store = new RedisStore({ const rateLimitStore: Store = new RedisStore({
prefix: "api-rate-limit", // Optional: customize Redis key prefix prefix: "api-rate-limit", // Optional: customize Redis key prefix
skipFailedRequests: true, // Don't count failed requests skipFailedRequests: true, // Don't count failed requests

View File

@@ -20,141 +20,151 @@ import { build } from "@server/build";
const portSchema = z.number().positive().gt(0).lte(65535); const portSchema = z.number().positive().gt(0).lte(65535);
export const privateConfigSchema = z export const privateConfigSchema = z.object({
.object({ app: z
app: z.object({ .object({
region: z.string().optional().default("default"), region: z.string().optional().default("default"),
base_domain: z.string().optional() base_domain: z.string().optional()
}).optional().default({ })
.optional()
.default({
region: "default" region: "default"
}), }),
server: z.object({ server: z
.object({
encryption_key_path: z encryption_key_path: z
.string() .string()
.optional() .optional()
.default("./config/encryption.pem") .default("./config/encryption.pem")
.pipe(z.string().min(8)), .pipe(z.string().min(8)),
resend_api_key: z.string().optional(), resend_api_key: z.string().optional(),
reo_client_id: z.string().optional(), reo_client_id: z.string().optional()
}).optional().default({ })
.optional()
.default({
encryption_key_path: "./config/encryption.pem" encryption_key_path: "./config/encryption.pem"
}), }),
redis: z redis: z
.object({ .object({
host: z.string(), host: z.string(),
port: portSchema, port: portSchema,
password: z.string().optional(), password: z.string().optional(),
db: z.number().int().nonnegative().optional().default(0), db: z.number().int().nonnegative().optional().default(0),
replicas: z replicas: z
.array( .array(
z.object({ z.object({
host: z.string(), host: z.string(),
port: portSchema, port: portSchema,
password: z.string().optional(), password: z.string().optional(),
db: z.number().int().nonnegative().optional().default(0) db: z.number().int().nonnegative().optional().default(0)
})
)
.optional()
// tls: z
// .object({
// reject_unauthorized: z
// .boolean()
// .optional()
// .default(true)
// })
// .optional()
})
.optional(),
gerbil: z
.object({
local_exit_node_reachable_at: z
.string()
.optional()
.default("http://gerbil:3003")
})
.optional()
.default({}),
flags: z
.object({
enable_redis: z.boolean().optional().default(false),
generate_own_certificates: z.boolean().optional().default(false)
})
.optional()
.default({}),
branding: z
.object({
app_name: z.string().optional(),
background_image_path: z.string().optional(),
colors: z
.object({
light: colorsSchema.optional(),
dark: colorsSchema.optional()
})
.optional(),
logo: z
.object({
light_path: z.string().optional(),
dark_path: z.string().optional(),
auth_page: z
.object({
width: z.number().optional(),
height: z.number().optional()
}) })
) .optional(),
.optional() navbar: z
// tls: z .object({
// .object({ width: z.number().optional(),
// reject_unauthorized: z height: z.number().optional()
// .boolean()
// .optional()
// .default(true)
// })
// .optional()
})
.optional(),
gerbil: z
.object({
local_exit_node_reachable_at: z.string().optional().default("http://gerbil:3003")
})
.optional()
.default({}),
flags: z
.object({
enable_redis: z.boolean().optional(),
})
.optional(),
branding: z
.object({
app_name: z.string().optional(),
background_image_path: z.string().optional(),
colors: z
.object({
light: colorsSchema.optional(),
dark: colorsSchema.optional()
})
.optional(),
logo: z
.object({
light_path: z.string().optional(),
dark_path: z.string().optional(),
auth_page: z
.object({
width: z.number().optional(),
height: z.number().optional()
})
.optional(),
navbar: z
.object({
width: z.number().optional(),
height: z.number().optional()
})
.optional()
})
.optional(),
favicon_path: z.string().optional(),
footer: z
.array(
z.object({
text: z.string(),
href: z.string().optional()
}) })
) .optional()
.optional(), })
login_page: z .optional(),
.object({ favicon_path: z.string().optional(),
subtitle_text: z.string().optional(), footer: z
title_text: z.string().optional() .array(
z.object({
text: z.string(),
href: z.string().optional()
}) })
.optional(), )
signup_page: z .optional(),
.object({ login_page: z
subtitle_text: z.string().optional(), .object({
title_text: z.string().optional() subtitle_text: z.string().optional(),
}) title_text: z.string().optional()
.optional(), })
resource_auth_page: z .optional(),
.object({ signup_page: z
show_logo: z.boolean().optional(), .object({
hide_powered_by: z.boolean().optional(), subtitle_text: z.string().optional(),
title_text: z.string().optional(), title_text: z.string().optional()
subtitle_text: z.string().optional() })
}) .optional(),
.optional(), resource_auth_page: z
emails: z .object({
.object({ show_logo: z.boolean().optional(),
signature: z.string().optional(), hide_powered_by: z.boolean().optional(),
colors: z title_text: z.string().optional(),
.object({ subtitle_text: z.string().optional()
primary: z.string().optional() })
}) .optional(),
.optional() emails: z
}) .object({
.optional() signature: z.string().optional(),
}) colors: z
.optional(), .object({
stripe: z primary: z.string().optional()
.object({ })
secret_key: z.string(), .optional()
webhook_secret: z.string(), })
s3Bucket: z.string(), .optional()
s3Region: z.string().default("us-east-1"), })
localFilePath: z.string() .optional(),
}) stripe: z
.optional(), .object({
}); secret_key: z.string(),
webhook_secret: z.string(),
s3Bucket: z.string(),
s3Region: z.string().default("us-east-1"),
localFilePath: z.string()
})
.optional()
});
export function readPrivateConfigFile() { export function readPrivateConfigFile() {
if (build == "oss") { if (build == "oss") {
@@ -182,9 +192,7 @@ export function readPrivateConfigFile() {
} }
if (!environment) { if (!environment) {
throw new Error( throw new Error("No private configuration file found.");
"No private configuration file found."
);
} }
return environment; return environment;

View File

@@ -46,7 +46,7 @@ class RedisManager {
this.isEnabled = false; this.isEnabled = false;
return; return;
} }
this.isEnabled = privateConfig.getRawPrivateConfig().flags?.enable_redis || false; this.isEnabled = privateConfig.getRawPrivateConfig().flags.enable_redis || false;
if (this.isEnabled) { if (this.isEnabled) {
this.initializeClients(); this.initializeClients();
} }

View File

@@ -21,11 +21,10 @@ import {
} from "@server/db"; } from "@server/db";
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm"; import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
import logger from "@server/logger"; import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { orgs, resources, sites, Target, targets } from "@server/db"; import { orgs, resources, sites, Target, targets } from "@server/db";
import { build } from "@server/build";
import { sanitize } from "@server/lib/traefik/utils"; import { sanitize } from "@server/lib/traefik/utils";
import privateConfig from "#private/lib/config";
const redirectHttpsMiddlewareName = "redirect-to-https"; const redirectHttpsMiddlewareName = "redirect-to-https";
const redirectToRootMiddlewareName = "redirect-to-root"; const redirectToRootMiddlewareName = "redirect-to-root";
@@ -79,7 +78,7 @@ export async function getTraefikConfig(
path: targets.path, path: targets.path,
pathMatchType: targets.pathMatchType, pathMatchType: targets.pathMatchType,
priority: targets.priority, priority: targets.priority,
// Site fields // Site fields
siteId: sites.siteId, siteId: sites.siteId,
siteType: sites.type, siteType: sites.type,
@@ -234,12 +233,13 @@ export async function getTraefikConfig(
continue; continue;
} }
if (resource.certificateStatus !== "valid") { // TODO: for now dont filter it out because if you have multiple domain ids and one is failed it causes all of them to fail
logger.debug( // if (resource.certificateStatus !== "valid" && privateConfig.getRawPrivateConfig().flags.generate_own_certificates) {
`Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}` // logger.debug(
); // `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
continue; // );
} // continue;
// }
// add routers and services empty objects if they don't exist // add routers and services empty objects if they don't exist
if (!config_output.http.routers) { if (!config_output.http.routers) {
@@ -264,18 +264,21 @@ export async function getTraefikConfig(
const configDomain = config.getDomain(resource.domainId); const configDomain = config.getDomain(resource.domainId);
let certResolver: string, preferWildcardCert: boolean;
if (!configDomain) {
certResolver = config.getRawConfig().traefik.cert_resolver;
preferWildcardCert =
config.getRawConfig().traefik.prefer_wildcard_cert;
} else {
certResolver = configDomain.cert_resolver;
preferWildcardCert = configDomain.prefer_wildcard_cert;
}
let tls = {}; let tls = {};
if (build == "oss") { if (
!privateConfig.getRawPrivateConfig().flags
.generate_own_certificates
) {
let certResolver: string, preferWildcardCert: boolean;
if (!configDomain) {
certResolver = config.getRawConfig().traefik.cert_resolver;
preferWildcardCert =
config.getRawConfig().traefik.prefer_wildcard_cert;
} else {
certResolver = configDomain.cert_resolver;
preferWildcardCert = configDomain.prefer_wildcard_cert;
}
tls = { tls = {
certResolver: certResolver, certResolver: certResolver,
...(preferWildcardCert ...(preferWildcardCert
@@ -419,7 +422,7 @@ export async function getTraefikConfig(
return ( return (
(targets as TargetWithSite[]) (targets as TargetWithSite[])
.filter((target: TargetWithSite) => { .filter((target: TargetWithSite) => {
if (!target.enabled) { if (!target.enabled) {
return false; return false;
} }
@@ -440,7 +443,7 @@ export async function getTraefikConfig(
) { ) {
return false; return false;
} }
} else if (target.site.type === "newt") { } else if (target.site.type === "newt") {
if ( if (
!target.internalPort || !target.internalPort ||
!target.method || !target.method ||
@@ -448,10 +451,10 @@ export async function getTraefikConfig(
) { ) {
return false; return false;
} }
} }
return true; return true;
}) })
.map((target: TargetWithSite) => { .map((target: TargetWithSite) => {
if ( if (
target.site.type === "local" || target.site.type === "local" ||
target.site.type === "wireguard" target.site.type === "wireguard"
@@ -459,14 +462,14 @@ export async function getTraefikConfig(
return { return {
url: `${target.method}://${target.ip}:${target.port}` url: `${target.method}://${target.ip}:${target.port}`
}; };
} else if (target.site.type === "newt") { } else if (target.site.type === "newt") {
const ip = const ip =
target.site.subnet!.split("/")[0]; target.site.subnet!.split("/")[0];
return { return {
url: `${target.method}://${ip}:${target.internalPort}` url: `${target.method}://${ip}:${target.internalPort}`
}; };
} }
}) })
// filter out duplicates // filter out duplicates
.filter( .filter(
(v, i, a) => (v, i, a) =>
@@ -709,4 +712,4 @@ export async function getTraefikConfig(
} }
return config_output; return config_output;
} }

View File

@@ -15,15 +15,19 @@ import { Certificate, certificates, db, domains } from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import { Transaction } from "@server/db"; import { Transaction } from "@server/db";
import { eq, or, and, like } from "drizzle-orm"; import { eq, or, and, like } from "drizzle-orm";
import { build } from "@server/build"; import privateConfig from "#private/lib/config";
/** /**
* Checks if a certificate exists for the given domain. * Checks if a certificate exists for the given domain.
* If not, creates a new certificate in 'pending' state. * If not, creates a new certificate in 'pending' state.
* Wildcard certs cover subdomains. * Wildcard certs cover subdomains.
*/ */
export async function createCertificate(domainId: string, domain: string, trx: Transaction | typeof db) { export async function createCertificate(
if (build !== "saas") { domainId: string,
domain: string,
trx: Transaction | typeof db
) {
if (!privateConfig.getRawPrivateConfig().flags.generate_own_certificates) {
return; return;
} }
@@ -39,7 +43,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
let existing: Certificate[] = []; let existing: Certificate[] = [];
if (domainRecord.type == "ns") { if (domainRecord.type == "ns") {
const domainLevelDown = domain.split('.').slice(1).join('.'); const domainLevelDown = domain.split(".").slice(1).join(".");
existing = await trx existing = await trx
.select() .select()
.from(certificates) .from(certificates)
@@ -49,7 +53,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
eq(certificates.wildcard, true), // only NS domains can have wildcard certs eq(certificates.wildcard, true), // only NS domains can have wildcard certs
or( or(
eq(certificates.domain, domain), eq(certificates.domain, domain),
eq(certificates.domain, domainLevelDown), eq(certificates.domain, domainLevelDown)
) )
) )
); );
@@ -67,9 +71,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
} }
if (existing.length > 0) { if (existing.length > 0) {
logger.info( logger.info(`Certificate already exists for domain ${domain}`);
`Certificate already exists for domain ${domain}`
);
return; return;
} }

View File

@@ -117,8 +117,8 @@ export default function ResourceRules(props: {
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const env = useEnvContext(); const { env } = useEnvContext();
const isMaxmindAvailable = env.env.server.maxmind_db_path && env.env.server.maxmind_db_path.length > 0; const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
const RuleAction = { const RuleAction = {
ACCEPT: t('alwaysAllow'), ACCEPT: t('alwaysAllow'),

View File

@@ -13,12 +13,14 @@ import {
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { build } from "@server/build"; import { build } from "@server/build";
import CertificateStatus from "@app/components/private/CertificateStatus"; import CertificateStatus from "@app/components/private/CertificateStatus";
import { toUnicode } from 'punycode'; import { toUnicode } from "punycode";
import { useEnvContext } from "@app/hooks/useEnvContext";
type ResourceInfoBoxType = {}; type ResourceInfoBoxType = {};
export default function ResourceInfoBox({ }: ResourceInfoBoxType) { export default function ResourceInfoBox({}: ResourceInfoBoxType) {
const { resource, authInfo } = useResourceContext(); const { resource, authInfo } = useResourceContext();
const { env } = useEnvContext();
const t = useTranslations(); const t = useTranslations();
@@ -28,7 +30,13 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
<Alert> <Alert>
<AlertDescription> <AlertDescription>
{/* 4 cols because of the certs */} {/* 4 cols because of the certs */}
<InfoSections cols={resource.http && build != "oss" ? 4 : 3}> <InfoSections
cols={
resource.http && env.flags.generateOwnCertificates
? 4
: 3
}
>
{resource.http ? ( {resource.http ? (
<> <>
<InfoSection> <InfoSection>
@@ -37,9 +45,9 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{authInfo.password || {authInfo.password ||
authInfo.pincode || authInfo.pincode ||
authInfo.sso || authInfo.sso ||
authInfo.whitelist ? ( authInfo.whitelist ? (
<div className="flex items-start space-x-2 text-green-500"> <div className="flex items-start space-x-2 text-green-500">
<ShieldCheck className="w-4 h-4 mt-0.5" /> <ShieldCheck className="w-4 h-4 mt-0.5" />
<span>{t("protected")}</span> <span>{t("protected")}</span>
@@ -126,25 +134,28 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
{/* </InfoSectionContent> */} {/* </InfoSectionContent> */}
{/* </InfoSection> */} {/* </InfoSection> */}
{/* Certificate Status Column */} {/* Certificate Status Column */}
{resource.http && resource.domainId && resource.fullDomain && build != "oss" && ( {resource.http &&
<InfoSection> resource.domainId &&
<InfoSectionTitle> resource.fullDomain &&
{t("certificateStatus", { build != "oss" && (
defaultValue: "Certificate" <InfoSection>
})} <InfoSectionTitle>
</InfoSectionTitle> {t("certificateStatus", {
<InfoSectionContent> defaultValue: "Certificate"
<CertificateStatus })}
orgId={resource.orgId} </InfoSectionTitle>
domainId={resource.domainId} <InfoSectionContent>
fullDomain={resource.fullDomain} <CertificateStatus
autoFetch={true} orgId={resource.orgId}
showLabel={false} domainId={resource.domainId}
polling={true} fullDomain={resource.fullDomain}
/> autoFetch={true}
</InfoSectionContent> showLabel={false}
</InfoSection> polling={true}
)} />
</InfoSectionContent>
</InfoSection>
)}
<InfoSection> <InfoSection>
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle> <InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>

View File

@@ -80,6 +80,7 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext();
const subscription = useSubscriptionStatusContext(); const subscription = useSubscriptionStatusContext();
const subscribed = subscription?.getTier() === TierId.STANDARD; const subscribed = subscription?.getTier() === TierId.STANDARD;
@@ -435,8 +436,8 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
</div> </div>
{/* Certificate Status */} {/* Certificate Status */}
{(build !== "saas" || {(
(build === "saas" && subscribed)) && (env.flags.generateOwnCertificates && subscribed)) &&
loginPage?.domainId && loginPage?.domainId &&
loginPage?.fullDomain && loginPage?.fullDomain &&
!hasUnsavedChanges && ( !hasUnsavedChanges && (

View File

@@ -48,7 +48,11 @@ export function pullEnv(): Env {
enableClients: enableClients:
process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false, process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false,
hideSupporterKey: hideSupporterKey:
process.env.HIDE_SUPPORTER_KEY === "true" ? true : false process.env.HIDE_SUPPORTER_KEY === "true" ? true : false,
generateOwnCertificates:
process.env.GENERATE_OWN_CERTIFICATES === "true"
? true
: false
}, },
branding: { branding: {

View File

@@ -28,6 +28,7 @@ export type Env = {
disableBasicWireguardSites: boolean; disableBasicWireguardSites: boolean;
enableClients: boolean; enableClients: boolean;
hideSupporterKey: boolean; hideSupporterKey: boolean;
generateOwnCertificates: boolean;
}, },
branding: { branding: {
appName?: string; appName?: string;