mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-15 08:49:59 +02:00
Move certificates
This commit is contained in:
@@ -29,25 +29,6 @@ import {
|
||||
labels
|
||||
} from "./schema";
|
||||
|
||||
export const certificates = pgTable("certificates", {
|
||||
certId: serial("certId").primaryKey(),
|
||||
domain: varchar("domain", { length: 255 }).notNull().unique(),
|
||||
domainId: varchar("domainId").references(() => domains.domainId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
wildcard: boolean("wildcard").default(false),
|
||||
status: varchar("status", { length: 50 }).notNull().default("pending"), // pending, requested, valid, expired, failed
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }),
|
||||
lastRenewalAttempt: bigint("lastRenewalAttempt", { mode: "number" }),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull(),
|
||||
orderId: varchar("orderId", { length: 500 }),
|
||||
errorMessage: text("errorMessage"),
|
||||
renewalCount: integer("renewalCount").default(0),
|
||||
certFile: text("certFile"),
|
||||
keyFile: text("keyFile")
|
||||
});
|
||||
|
||||
export const dnsChallenge = pgTable("dnsChallenges", {
|
||||
dnsChallengeId: serial("dnsChallengeId").primaryKey(),
|
||||
domain: varchar("domain", { length: 255 }).notNull(),
|
||||
@@ -633,7 +614,6 @@ export const trialNotifications = pgTable("trialNotifications", {
|
||||
export type Approval = InferSelectModel<typeof approvals>;
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
export type Certificate = InferSelectModel<typeof certificates>;
|
||||
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
||||
export type Customer = InferSelectModel<typeof customers>;
|
||||
export type Subscription = InferSelectModel<typeof subscriptions>;
|
||||
|
||||
@@ -2017,6 +2017,25 @@ export const aiSessionLog = pgTable(
|
||||
]
|
||||
);
|
||||
|
||||
export const certificates = pgTable("certificates", {
|
||||
certId: serial("certId").primaryKey(),
|
||||
domain: varchar("domain", { length: 255 }).notNull().unique(),
|
||||
domainId: varchar("domainId").references(() => domains.domainId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
wildcard: boolean("wildcard").default(false),
|
||||
status: varchar("status", { length: 50 }).notNull().default("pending"), // pending, requested, valid, expired, failed
|
||||
expiresAt: bigint("expiresAt", { mode: "number" }),
|
||||
lastRenewalAttempt: bigint("lastRenewalAttempt", { mode: "number" }),
|
||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||
updatedAt: bigint("updatedAt", { mode: "number" }).notNull(),
|
||||
orderId: varchar("orderId", { length: 500 }),
|
||||
errorMessage: text("errorMessage"),
|
||||
renewalCount: integer("renewalCount").default(0),
|
||||
certFile: text("certFile"),
|
||||
keyFile: text("keyFile")
|
||||
});
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
@@ -2117,3 +2136,4 @@ export type SiteResourceAiProvider = InferSelectModel<
|
||||
>;
|
||||
export type ResourceAiModel = InferSelectModel<typeof resourceAiModels>;
|
||||
export type SiteResourceAiModel = InferSelectModel<typeof siteResourceAiModels>;
|
||||
export type Certificate = InferSelectModel<typeof certificates>;
|
||||
|
||||
@@ -23,25 +23,6 @@ import {
|
||||
users
|
||||
} from "./schema";
|
||||
|
||||
export const certificates = sqliteTable("certificates", {
|
||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
||||
domain: text("domain").notNull().unique(),
|
||||
domainId: text("domainId").references(() => domains.domainId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
wildcard: integer("wildcard", { mode: "boolean" }).default(false),
|
||||
status: text("status").notNull().default("pending"), // pending, requested, valid, expired, failed
|
||||
expiresAt: integer("expiresAt"),
|
||||
lastRenewalAttempt: integer("lastRenewalAttempt"),
|
||||
createdAt: integer("createdAt").notNull(),
|
||||
updatedAt: integer("updatedAt").notNull(),
|
||||
orderId: text("orderId"),
|
||||
errorMessage: text("errorMessage"),
|
||||
renewalCount: integer("renewalCount").default(0),
|
||||
certFile: text("certFile"),
|
||||
keyFile: text("keyFile")
|
||||
});
|
||||
|
||||
export const dnsChallenge = sqliteTable("dnsChallenges", {
|
||||
dnsChallengeId: integer("dnsChallengeId").primaryKey({
|
||||
autoIncrement: true
|
||||
@@ -628,7 +609,6 @@ export const trialNotifications = sqliteTable("trialNotifications", {
|
||||
export type Approval = InferSelectModel<typeof approvals>;
|
||||
export type Limit = InferSelectModel<typeof limits>;
|
||||
export type Account = InferSelectModel<typeof account>;
|
||||
export type Certificate = InferSelectModel<typeof certificates>;
|
||||
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
||||
export type Customer = InferSelectModel<typeof customers>;
|
||||
export type Subscription = InferSelectModel<typeof subscriptions>;
|
||||
|
||||
@@ -2013,6 +2013,25 @@ export const aiSessionLog = sqliteTable(
|
||||
]
|
||||
);
|
||||
|
||||
export const certificates = sqliteTable("certificates", {
|
||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
||||
domain: text("domain").notNull().unique(),
|
||||
domainId: text("domainId").references(() => domains.domainId, {
|
||||
onDelete: "cascade"
|
||||
}),
|
||||
wildcard: integer("wildcard", { mode: "boolean" }).default(false),
|
||||
status: text("status").notNull().default("pending"), // pending, requested, valid, expired, failed
|
||||
expiresAt: integer("expiresAt"),
|
||||
lastRenewalAttempt: integer("lastRenewalAttempt"),
|
||||
createdAt: integer("createdAt").notNull(),
|
||||
updatedAt: integer("updatedAt").notNull(),
|
||||
orderId: text("orderId"),
|
||||
errorMessage: text("errorMessage"),
|
||||
renewalCount: integer("renewalCount").default(0),
|
||||
certFile: text("certFile"),
|
||||
keyFile: text("keyFile")
|
||||
});
|
||||
|
||||
export type Org = InferSelectModel<typeof orgs>;
|
||||
export type User = InferSelectModel<typeof users>;
|
||||
export type Site = InferSelectModel<typeof sites>;
|
||||
@@ -2111,3 +2130,4 @@ export type SiteResourceAiProvider = InferSelectModel<
|
||||
>;
|
||||
export type ResourceAiModel = InferSelectModel<typeof resourceAiModels>;
|
||||
export type SiteResourceAiModel = InferSelectModel<typeof siteResourceAiModels>;
|
||||
export type Certificate = InferSelectModel<typeof certificates>;
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
|
||||
import { initCleanup } from "#dynamic/cleanup";
|
||||
import license from "#dynamic/license/license";
|
||||
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
||||
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
|
||||
import { initAcmeCertSync } from "@server/lib/acmeCertSync";
|
||||
import { fetchServerIp } from "@server/lib/serverIpService";
|
||||
import { startRebuildQueueProcessor } from "@server/lib/rebuildClientAssociations";
|
||||
import { initAiModelCatalog } from "@server/lib/aiModelCatalog";
|
||||
|
||||
@@ -23,7 +23,7 @@ import { getOrCreateLabelIds, syncSiteResourceLabels } from "./labels";
|
||||
import logger from "@server/logger";
|
||||
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
||||
import { getNextAvailableAliasAddress } from "../ip";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { createCertificate } from "@server/routers/certificates/createCertificate";
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { tierMatrix } from "../billing/tierMatrix";
|
||||
import { build } from "@server/build";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { createCertificate } from "@server/routers/certificates/createCertificate";
|
||||
import { hashPassword } from "@server/auth/password";
|
||||
import { generateId } from "@server/auth/sessions/app";
|
||||
import { build } from "@server/build";
|
||||
|
||||
+222
-13
@@ -1,17 +1,226 @@
|
||||
import config from "@server/lib/config";
|
||||
import { certificates, db } from "@server/db";
|
||||
import { and, eq, isNotNull, or, inArray, sql } from "drizzle-orm";
|
||||
import { decrypt } from "@server/lib/crypto";
|
||||
import logger from "@server/logger";
|
||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
||||
import { build } from "@server/build";
|
||||
|
||||
// Define the return type for clarity and type safety
|
||||
export type CertificateResult = {
|
||||
id: number;
|
||||
domain: string;
|
||||
queriedDomain: string; // The domain that was originally requested (may differ for wildcards)
|
||||
wildcard: boolean | null;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: number | null;
|
||||
updatedAt?: number | null;
|
||||
};
|
||||
|
||||
export async function getValidCertificatesForDomains(
|
||||
domains: Set<string>,
|
||||
useCache: boolean = true
|
||||
): Promise<
|
||||
Array<{
|
||||
id: number;
|
||||
domain: string;
|
||||
queriedDomain: string;
|
||||
wildcard: boolean | null;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: number | null;
|
||||
updatedAt?: number | null;
|
||||
}>
|
||||
> {
|
||||
return []; // stub
|
||||
): Promise<Array<CertificateResult>> {
|
||||
const finalResults: CertificateResult[] = [];
|
||||
const domainsToQuery = new Set<string>();
|
||||
|
||||
// 1. Check cache first if enabled
|
||||
if (useCache) {
|
||||
for (const domain of domains) {
|
||||
const cacheKey = `cert:${domain}`;
|
||||
const cachedCert = await cache.get<CertificateResult>(cacheKey);
|
||||
if (cachedCert) {
|
||||
finalResults.push(cachedCert); // Valid cache hit
|
||||
} else {
|
||||
// Also check for a wildcard cache entry covering this domain's parent
|
||||
const parts = domain.split(".");
|
||||
let wildcardHit = false;
|
||||
if (parts.length > 1) {
|
||||
const parentDomain = parts.slice(1).join(".");
|
||||
const wildcardCacheKey = `cert:*.${parentDomain}`;
|
||||
const cachedWildcard =
|
||||
await cache.get<CertificateResult>(wildcardCacheKey);
|
||||
if (cachedWildcard) {
|
||||
// Re-stamp queriedDomain so callers see the originally requested domain
|
||||
finalResults.push({
|
||||
...cachedWildcard,
|
||||
queriedDomain: domain
|
||||
});
|
||||
wildcardHit = true;
|
||||
}
|
||||
}
|
||||
if (!wildcardHit) {
|
||||
domainsToQuery.add(domain); // Cache miss or expired
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If caching is disabled, add all domains to the query set
|
||||
domains.forEach((d) => domainsToQuery.add(d));
|
||||
}
|
||||
|
||||
// 2. If all domains were resolved from the cache, return early
|
||||
if (domainsToQuery.size === 0) {
|
||||
const decryptedResults = decryptFinalResults(
|
||||
finalResults,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
return decryptedResults;
|
||||
}
|
||||
|
||||
// 3. Prepare domains for the database query
|
||||
const domainsToQueryArray = Array.from(domainsToQuery);
|
||||
const parentDomainsToQuery = new Set<string>();
|
||||
|
||||
domainsToQueryArray.forEach((domain) => {
|
||||
const parts = domain.split(".");
|
||||
// A wildcard can only match a domain with at least two parts (e.g., example.com)
|
||||
if (parts.length > 1) {
|
||||
parentDomainsToQuery.add(parts.slice(1).join("."));
|
||||
}
|
||||
});
|
||||
|
||||
const parentDomainsArray = Array.from(parentDomainsToQuery);
|
||||
|
||||
// Build wildcard variants: for each parent domain "example.com", also query "*.example.com"
|
||||
const wildcardPrefixedArray =
|
||||
build != "saas" ? parentDomainsArray.map((d) => `*.${d}`) : [];
|
||||
|
||||
// 4. Build and execute a single, efficient Drizzle query
|
||||
// This query fetches all potential exact and wildcard matches in one database round-trip.
|
||||
const potentialCerts = await db
|
||||
.select()
|
||||
.from(certificates)
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.status, "valid"),
|
||||
isNotNull(certificates.certFile),
|
||||
isNotNull(certificates.keyFile),
|
||||
or(
|
||||
// Condition for exact matches on the requested domains
|
||||
inArray(certificates.domain, domainsToQueryArray),
|
||||
// Condition for wildcard matches on the parent domains (stored as "example.com" or "*.example.com")
|
||||
parentDomainsArray.length > 0
|
||||
? and(
|
||||
inArray(certificates.domain, [
|
||||
...parentDomainsArray,
|
||||
...wildcardPrefixedArray
|
||||
]),
|
||||
eq(certificates.wildcard, true)
|
||||
)
|
||||
: // If there are no possible parent domains, this condition is false
|
||||
sql`false`
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Helper to normalize a wildcard cert's domain to its bare parent domain (strips leading "*.")
|
||||
const normalizeWildcardDomain = (domain: string): string =>
|
||||
domain.startsWith("*.") ? domain.slice(2) : domain;
|
||||
|
||||
// 5. Process the database results, prioritizing exact matches over wildcards
|
||||
const exactMatches = new Map<string, (typeof potentialCerts)[0]>();
|
||||
const wildcardMatches = new Map<string, (typeof potentialCerts)[0]>();
|
||||
|
||||
for (const cert of potentialCerts) {
|
||||
if (cert.wildcard) {
|
||||
// Normalize to bare parent domain so lookups are consistent regardless of storage format
|
||||
wildcardMatches.set(normalizeWildcardDomain(cert.domain), cert);
|
||||
} else {
|
||||
exactMatches.set(cert.domain, cert);
|
||||
}
|
||||
}
|
||||
|
||||
for (const domain of domainsToQuery) {
|
||||
let foundCert: (typeof potentialCerts)[0] | undefined = undefined;
|
||||
|
||||
// Priority 1: Check for an exact match (non-wildcard)
|
||||
if (exactMatches.has(domain)) {
|
||||
foundCert = exactMatches.get(domain);
|
||||
}
|
||||
// Priority 2: Check for a wildcard certificate whose normalized domain equals the queried domain
|
||||
else {
|
||||
const normalizedDomain = normalizeWildcardDomain(domain);
|
||||
if (wildcardMatches.has(normalizedDomain)) {
|
||||
foundCert = wildcardMatches.get(normalizedDomain);
|
||||
}
|
||||
// Priority 3: Check for a wildcard match on the parent domain
|
||||
else {
|
||||
const parts = normalizedDomain.split(".");
|
||||
if (parts.length > 1) {
|
||||
const parentDomain = parts.slice(1).join(".");
|
||||
if (wildcardMatches.has(parentDomain)) {
|
||||
foundCert = wildcardMatches.get(parentDomain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a certificate was found, format it, add to results, and cache it
|
||||
if (foundCert) {
|
||||
logger.debug(
|
||||
`Creating result cert for ${domain} using cert from ${foundCert.domain}`
|
||||
);
|
||||
const resultCert: CertificateResult = {
|
||||
id: foundCert.certId,
|
||||
domain: foundCert.domain, // The actual domain of the cert record
|
||||
queriedDomain: domain, // The domain that was originally requested
|
||||
wildcard: foundCert.wildcard,
|
||||
certFile: foundCert.certFile,
|
||||
keyFile: foundCert.keyFile,
|
||||
expiresAt: foundCert.expiresAt,
|
||||
updatedAt: foundCert.updatedAt
|
||||
};
|
||||
|
||||
finalResults.push(resultCert);
|
||||
|
||||
// Add to cache for future requests, using the *requested domain* as the key
|
||||
if (useCache) {
|
||||
const cacheKey = `cert:${domain}`;
|
||||
await cache.set(cacheKey, resultCert, 180);
|
||||
|
||||
// Also cache wildcard certs under a pattern key so other subdomains
|
||||
// can find them without a DB round-trip
|
||||
if (resultCert.wildcard) {
|
||||
const normalizedCertDomain = normalizeWildcardDomain(
|
||||
resultCert.domain
|
||||
);
|
||||
const wildcardCacheKey = `cert:*.${normalizedCertDomain}`;
|
||||
await cache.set(wildcardCacheKey, resultCert, 180);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decryptedResults = decryptFinalResults(
|
||||
finalResults,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
return decryptedResults;
|
||||
}
|
||||
|
||||
function decryptFinalResults(
|
||||
finalResults: CertificateResult[],
|
||||
secret: string
|
||||
): CertificateResult[] {
|
||||
const validCertsDecrypted = finalResults.map((cert) => {
|
||||
// Decrypt and save certificate file
|
||||
const decryptedCert = decrypt(
|
||||
cert.certFile!, // is not null from query
|
||||
secret
|
||||
);
|
||||
|
||||
// Decrypt and save key file
|
||||
const decryptedKey = decrypt(cert.keyFile!, secret);
|
||||
|
||||
// Return only the certificate data without org information
|
||||
return {
|
||||
...cert,
|
||||
certFile: decryptedCert,
|
||||
keyFile: decryptedKey
|
||||
};
|
||||
});
|
||||
|
||||
return validCertsDecrypted;
|
||||
}
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import z from "zod";
|
||||
import logger from "@server/logger";
|
||||
import semver from "semver";
|
||||
import { createHash } from "crypto";
|
||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||
import { getValidCertificatesForDomains } from "@server/lib/certificates";
|
||||
import { lockManager } from "#dynamic/lib/lock";
|
||||
|
||||
interface IPRange {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { db, exitNodes } from "@server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
||||
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||
import { getValidCertificatesForDomains } from "@server/lib/certificates";
|
||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||
import { build } from "@server/build";
|
||||
|
||||
@@ -628,8 +628,7 @@ export class TraefikConfigManager {
|
||||
.name,
|
||||
|
||||
remoteRoleHeader:
|
||||
config.getRawConfig().server.remote_headers
|
||||
.role
|
||||
config.getRawConfig().server.remote_headers.role
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -38,3 +38,4 @@ export * from "./logActionAudit";
|
||||
export * from "./verifyOlmAccess";
|
||||
export * from "./verifyLimits";
|
||||
export * from "./verifyResourcePolicyAccess";
|
||||
export * from "./verifyCertificateAccess";
|
||||
|
||||
-13
@@ -1,16 +1,3 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { db, domainNamespaces } from "@server/db";
|
||||
import { certificates } from "@server/db";
|
||||
@@ -1,240 +0,0 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import privateConfig from "./config";
|
||||
import config from "@server/lib/config";
|
||||
import { certificates, db } from "@server/db";
|
||||
import { and, eq, isNotNull, or, inArray, sql } from "drizzle-orm";
|
||||
import { decrypt } from "@server/lib/crypto";
|
||||
import logger from "@server/logger";
|
||||
import { regionalCache as cache } from "#private/lib/cache";
|
||||
import { build } from "@server/build";
|
||||
|
||||
// Define the return type for clarity and type safety
|
||||
export type CertificateResult = {
|
||||
id: number;
|
||||
domain: string;
|
||||
queriedDomain: string; // The domain that was originally requested (may differ for wildcards)
|
||||
wildcard: boolean | null;
|
||||
certFile: string | null;
|
||||
keyFile: string | null;
|
||||
expiresAt: number | null;
|
||||
updatedAt?: number | null;
|
||||
};
|
||||
|
||||
export async function getValidCertificatesForDomains(
|
||||
domains: Set<string>,
|
||||
useCache: boolean = true
|
||||
): Promise<Array<CertificateResult>> {
|
||||
const finalResults: CertificateResult[] = [];
|
||||
const domainsToQuery = new Set<string>();
|
||||
|
||||
// 1. Check cache first if enabled
|
||||
if (useCache) {
|
||||
for (const domain of domains) {
|
||||
const cacheKey = `cert:${domain}`;
|
||||
const cachedCert = await cache.get<CertificateResult>(cacheKey);
|
||||
if (cachedCert) {
|
||||
finalResults.push(cachedCert); // Valid cache hit
|
||||
} else {
|
||||
// Also check for a wildcard cache entry covering this domain's parent
|
||||
const parts = domain.split(".");
|
||||
let wildcardHit = false;
|
||||
if (parts.length > 1) {
|
||||
const parentDomain = parts.slice(1).join(".");
|
||||
const wildcardCacheKey = `cert:*.${parentDomain}`;
|
||||
const cachedWildcard =
|
||||
await cache.get<CertificateResult>(wildcardCacheKey);
|
||||
if (cachedWildcard) {
|
||||
// Re-stamp queriedDomain so callers see the originally requested domain
|
||||
finalResults.push({
|
||||
...cachedWildcard,
|
||||
queriedDomain: domain
|
||||
});
|
||||
wildcardHit = true;
|
||||
}
|
||||
}
|
||||
if (!wildcardHit) {
|
||||
domainsToQuery.add(domain); // Cache miss or expired
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If caching is disabled, add all domains to the query set
|
||||
domains.forEach((d) => domainsToQuery.add(d));
|
||||
}
|
||||
|
||||
// 2. If all domains were resolved from the cache, return early
|
||||
if (domainsToQuery.size === 0) {
|
||||
const decryptedResults = decryptFinalResults(
|
||||
finalResults,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
return decryptedResults;
|
||||
}
|
||||
|
||||
// 3. Prepare domains for the database query
|
||||
const domainsToQueryArray = Array.from(domainsToQuery);
|
||||
const parentDomainsToQuery = new Set<string>();
|
||||
|
||||
domainsToQueryArray.forEach((domain) => {
|
||||
const parts = domain.split(".");
|
||||
// A wildcard can only match a domain with at least two parts (e.g., example.com)
|
||||
if (parts.length > 1) {
|
||||
parentDomainsToQuery.add(parts.slice(1).join("."));
|
||||
}
|
||||
});
|
||||
|
||||
const parentDomainsArray = Array.from(parentDomainsToQuery);
|
||||
|
||||
// Build wildcard variants: for each parent domain "example.com", also query "*.example.com"
|
||||
const wildcardPrefixedArray =
|
||||
build != "saas" ? parentDomainsArray.map((d) => `*.${d}`) : [];
|
||||
|
||||
// 4. Build and execute a single, efficient Drizzle query
|
||||
// This query fetches all potential exact and wildcard matches in one database round-trip.
|
||||
const potentialCerts = await db
|
||||
.select()
|
||||
.from(certificates)
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.status, "valid"),
|
||||
isNotNull(certificates.certFile),
|
||||
isNotNull(certificates.keyFile),
|
||||
or(
|
||||
// Condition for exact matches on the requested domains
|
||||
inArray(certificates.domain, domainsToQueryArray),
|
||||
// Condition for wildcard matches on the parent domains (stored as "example.com" or "*.example.com")
|
||||
parentDomainsArray.length > 0
|
||||
? and(
|
||||
inArray(certificates.domain, [
|
||||
...parentDomainsArray,
|
||||
...wildcardPrefixedArray
|
||||
]),
|
||||
eq(certificates.wildcard, true)
|
||||
)
|
||||
: // If there are no possible parent domains, this condition is false
|
||||
sql`false`
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
// Helper to normalize a wildcard cert's domain to its bare parent domain (strips leading "*.")
|
||||
const normalizeWildcardDomain = (domain: string): string =>
|
||||
domain.startsWith("*.") ? domain.slice(2) : domain;
|
||||
|
||||
// 5. Process the database results, prioritizing exact matches over wildcards
|
||||
const exactMatches = new Map<string, (typeof potentialCerts)[0]>();
|
||||
const wildcardMatches = new Map<string, (typeof potentialCerts)[0]>();
|
||||
|
||||
for (const cert of potentialCerts) {
|
||||
if (cert.wildcard) {
|
||||
// Normalize to bare parent domain so lookups are consistent regardless of storage format
|
||||
wildcardMatches.set(normalizeWildcardDomain(cert.domain), cert);
|
||||
} else {
|
||||
exactMatches.set(cert.domain, cert);
|
||||
}
|
||||
}
|
||||
|
||||
for (const domain of domainsToQuery) {
|
||||
let foundCert: (typeof potentialCerts)[0] | undefined = undefined;
|
||||
|
||||
// Priority 1: Check for an exact match (non-wildcard)
|
||||
if (exactMatches.has(domain)) {
|
||||
foundCert = exactMatches.get(domain);
|
||||
}
|
||||
// Priority 2: Check for a wildcard certificate whose normalized domain equals the queried domain
|
||||
else {
|
||||
const normalizedDomain = normalizeWildcardDomain(domain);
|
||||
if (wildcardMatches.has(normalizedDomain)) {
|
||||
foundCert = wildcardMatches.get(normalizedDomain);
|
||||
}
|
||||
// Priority 3: Check for a wildcard match on the parent domain
|
||||
else {
|
||||
const parts = normalizedDomain.split(".");
|
||||
if (parts.length > 1) {
|
||||
const parentDomain = parts.slice(1).join(".");
|
||||
if (wildcardMatches.has(parentDomain)) {
|
||||
foundCert = wildcardMatches.get(parentDomain);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a certificate was found, format it, add to results, and cache it
|
||||
if (foundCert) {
|
||||
logger.debug(
|
||||
`Creating result cert for ${domain} using cert from ${foundCert.domain}`
|
||||
);
|
||||
const resultCert: CertificateResult = {
|
||||
id: foundCert.certId,
|
||||
domain: foundCert.domain, // The actual domain of the cert record
|
||||
queriedDomain: domain, // The domain that was originally requested
|
||||
wildcard: foundCert.wildcard,
|
||||
certFile: foundCert.certFile,
|
||||
keyFile: foundCert.keyFile,
|
||||
expiresAt: foundCert.expiresAt,
|
||||
updatedAt: foundCert.updatedAt
|
||||
};
|
||||
|
||||
finalResults.push(resultCert);
|
||||
|
||||
// Add to cache for future requests, using the *requested domain* as the key
|
||||
if (useCache) {
|
||||
const cacheKey = `cert:${domain}`;
|
||||
await cache.set(cacheKey, resultCert, 180);
|
||||
|
||||
// Also cache wildcard certs under a pattern key so other subdomains
|
||||
// can find them without a DB round-trip
|
||||
if (resultCert.wildcard) {
|
||||
const normalizedCertDomain = normalizeWildcardDomain(
|
||||
resultCert.domain
|
||||
);
|
||||
const wildcardCacheKey = `cert:*.${normalizedCertDomain}`;
|
||||
await cache.set(wildcardCacheKey, resultCert, 180);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const decryptedResults = decryptFinalResults(
|
||||
finalResults,
|
||||
config.getRawConfig().server.secret!
|
||||
);
|
||||
return decryptedResults;
|
||||
}
|
||||
|
||||
function decryptFinalResults(
|
||||
finalResults: CertificateResult[],
|
||||
secret: string
|
||||
): CertificateResult[] {
|
||||
const validCertsDecrypted = finalResults.map((cert) => {
|
||||
// Decrypt and save certificate file
|
||||
const decryptedCert = decrypt(
|
||||
cert.certFile!, // is not null from query
|
||||
secret
|
||||
);
|
||||
|
||||
// Decrypt and save key file
|
||||
const decryptedKey = decrypt(cert.keyFile!, secret);
|
||||
|
||||
// Return only the certificate data without org information
|
||||
return {
|
||||
...cert,
|
||||
certFile: decryptedCert,
|
||||
keyFile: decryptedKey
|
||||
};
|
||||
});
|
||||
|
||||
return validCertsDecrypted;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ import createPathRewriteMiddleware from "@server/lib/traefik/middleware";
|
||||
import {
|
||||
CertificateResult,
|
||||
getValidCertificatesForDomains
|
||||
} from "#private/lib/certificates";
|
||||
} from "@server/lib/certificates";
|
||||
import { build } from "@server/build";
|
||||
import regionalCache from "#private/lib/cache";
|
||||
import {
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
export * from "./verifyCertificateAccess";
|
||||
export * from "./verifyRemoteExitNodeAccess";
|
||||
export * from "./verifyIdpAccess";
|
||||
export * from "./verifyLoginPageAccess";
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { Certificate, certificates, db, domains } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { Transaction } from "@server/db";
|
||||
import { eq, or, and, like } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Checks if a certificate exists for the given domain.
|
||||
* If not, creates a new certificate in 'pending' state.
|
||||
* Wildcard certs cover subdomains.
|
||||
*/
|
||||
export async function createCertificate(
|
||||
domainId: string,
|
||||
domain: string,
|
||||
trx: Transaction | typeof db
|
||||
) {
|
||||
const [domainRecord] = await trx
|
||||
.select()
|
||||
.from(domains)
|
||||
.where(eq(domains.domainId, domainId))
|
||||
.limit(1);
|
||||
|
||||
if (!domainRecord) {
|
||||
throw new Error(`Domain with ID ${domainId} not found`);
|
||||
}
|
||||
|
||||
let existing: Certificate[] = [];
|
||||
if (domainRecord.type == "ns" || domainRecord.type == "wildcard") {
|
||||
const domainLevelDown = domain.split(".").slice(1).join(".");
|
||||
const wildcardPrefixed = `*.${domainLevelDown}`;
|
||||
|
||||
existing = await trx
|
||||
.select()
|
||||
.from(certificates)
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.domainId, domainId),
|
||||
or(
|
||||
eq(certificates.domain, domain),
|
||||
and(
|
||||
eq(certificates.wildcard, true),
|
||||
or(
|
||||
eq(certificates.domain, domainLevelDown),
|
||||
eq(certificates.domain, wildcardPrefixed)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// For non-NS domains, we only match exact domain names
|
||||
existing = await trx
|
||||
.select()
|
||||
.from(certificates)
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.domainId, domainId),
|
||||
eq(certificates.domain, domain) // exact match for non-NS domains
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (existing.length > 0) {
|
||||
logger.info(`Certificate already exists for domain ${domain}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let domainToWrite = domain;
|
||||
if (
|
||||
domainRecord.type == "wildcard" && // this is to fix the wildcard certs for traefik in self hosted NOT ON THE CLOUD
|
||||
domainRecord.preferWildcardCert &&
|
||||
!domain.startsWith("*.")
|
||||
) {
|
||||
// in this case traefik is going to generate a domain one level down so we need to store it that way
|
||||
const parts = domain.split(".");
|
||||
if (parts.length > 2) {
|
||||
domainToWrite = parts.slice(1).join(".");
|
||||
domainToWrite = `*.${domainToWrite}`;
|
||||
}
|
||||
} else if (domainRecord.type == "ns") {
|
||||
if (domain == domainRecord.baseDomain) {
|
||||
domainToWrite = domainRecord.baseDomain;
|
||||
} else {
|
||||
const parts = domain.split(".");
|
||||
if (parts.length > 2) {
|
||||
domainToWrite = parts.slice(1).join(".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No cert found, create a new one in pending state
|
||||
await trx.insert(certificates).values({
|
||||
domain: domainToWrite,
|
||||
domainId,
|
||||
wildcard:
|
||||
domainRecord.type == "ns" ||
|
||||
(domainRecord.type == "wildcard" &&
|
||||
domainRecord.preferWildcardCert), // we can only create wildcard certs for NS domains
|
||||
status: "pending",
|
||||
updatedAt: Math.floor(Date.now() / 1000),
|
||||
createdAt: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
export * from "./getCertificate";
|
||||
export * from "./restartCertificate";
|
||||
export * from "./syncCertToNewts";
|
||||
export * from "./getBatchedCertificates";
|
||||
@@ -11,7 +11,6 @@
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import * as certificates from "#private/routers/certificates";
|
||||
import { createStore } from "#private/lib/rateLimitStore";
|
||||
import * as billing from "#private/routers/billing";
|
||||
import * as remoteExitNode from "#private/routers/remoteExitNode";
|
||||
@@ -50,7 +49,6 @@ import {
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import {
|
||||
logActionAudit,
|
||||
verifyCertificateAccess,
|
||||
verifyIdpAccess,
|
||||
verifyLoginPageAccess,
|
||||
verifyRemoteExitNodeAccess,
|
||||
@@ -164,32 +162,6 @@ authenticated.get(
|
||||
orgIdp.listUserAdminOrgIdps
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/certificate/:domainId/:domain",
|
||||
verifyOrgAccess,
|
||||
verifyCertificateAccess,
|
||||
verifyUserHasAction(ActionsEnum.getCertificate),
|
||||
certificates.getCertificate
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/batched-certificates",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.getCertificate),
|
||||
certificates.getBatchedCertificates
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/certificate/:certId/restart",
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyCertificateAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.restartCertificate),
|
||||
logActionAudit(ActionsEnum.restartCertificate),
|
||||
certificates.restartCertificate
|
||||
);
|
||||
|
||||
if (build === "saas") {
|
||||
authenticated.post(
|
||||
"/org/:orgId/billing/create-checkout-session",
|
||||
|
||||
@@ -15,7 +15,7 @@ import * as orgIdp from "#private/routers/orgIdp";
|
||||
import * as org from "#private/routers/org";
|
||||
import * as logs from "#private/routers/auditLogs";
|
||||
import * as alertEvents from "#private/routers/alertEvents";
|
||||
import * as certificates from "#private/routers/certificates";
|
||||
import * as certificates from "@server/routers/certificates";
|
||||
import * as siteProvisioning from "#private/routers/siteProvisioning";
|
||||
import * as policy from "#private/routers/policy";
|
||||
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
||||
|
||||
@@ -29,7 +29,7 @@ import logger from "@server/logger";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||
import { createCertificate } from "#private/routers/certificates/createCertificate";
|
||||
import { createCertificate } from "@server/routers/certificates/createCertificate";
|
||||
|
||||
import { CreateLoginPageResponse } from "@server/routers/loginPage/types";
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import { fromError } from "zod-validation-error";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||
import { subdomainSchema } from "@server/lib/schemas";
|
||||
import { createCertificate } from "#private/routers/certificates/createCertificate";
|
||||
import { createCertificate } from "@server/routers/certificates/createCertificate";
|
||||
|
||||
import { UpdateLoginPageResponse } from "@server/routers/loginPage/types";
|
||||
|
||||
@@ -85,7 +85,6 @@ export async function updateLoginPage(
|
||||
|
||||
const { loginPageId, orgId } = parsedParams.data;
|
||||
|
||||
|
||||
const [existingLoginPage] = await db
|
||||
.select()
|
||||
.from(loginPage)
|
||||
|
||||
@@ -1,9 +1,102 @@
|
||||
import { db, Transaction } from "@server/db";
|
||||
import { Certificate, certificates, db, domains } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { Transaction } from "@server/db";
|
||||
import { eq, or, and, like } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Checks if a certificate exists for the given domain.
|
||||
* If not, creates a new certificate in 'pending' state.
|
||||
* Wildcard certs cover subdomains.
|
||||
*/
|
||||
export async function createCertificate(
|
||||
domainId: string,
|
||||
domain: string,
|
||||
trx: Transaction | typeof db
|
||||
) {
|
||||
return;
|
||||
const [domainRecord] = await trx
|
||||
.select()
|
||||
.from(domains)
|
||||
.where(eq(domains.domainId, domainId))
|
||||
.limit(1);
|
||||
|
||||
if (!domainRecord) {
|
||||
throw new Error(`Domain with ID ${domainId} not found`);
|
||||
}
|
||||
|
||||
let existing: Certificate[] = [];
|
||||
if (domainRecord.type == "ns" || domainRecord.type == "wildcard") {
|
||||
const domainLevelDown = domain.split(".").slice(1).join(".");
|
||||
const wildcardPrefixed = `*.${domainLevelDown}`;
|
||||
|
||||
existing = await trx
|
||||
.select()
|
||||
.from(certificates)
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.domainId, domainId),
|
||||
or(
|
||||
eq(certificates.domain, domain),
|
||||
and(
|
||||
eq(certificates.wildcard, true),
|
||||
or(
|
||||
eq(certificates.domain, domainLevelDown),
|
||||
eq(certificates.domain, wildcardPrefixed)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// For non-NS domains, we only match exact domain names
|
||||
existing = await trx
|
||||
.select()
|
||||
.from(certificates)
|
||||
.where(
|
||||
and(
|
||||
eq(certificates.domainId, domainId),
|
||||
eq(certificates.domain, domain) // exact match for non-NS domains
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (existing.length > 0) {
|
||||
logger.info(`Certificate already exists for domain ${domain}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let domainToWrite = domain;
|
||||
if (
|
||||
domainRecord.type == "wildcard" && // this is to fix the wildcard certs for traefik in self hosted NOT ON THE CLOUD
|
||||
domainRecord.preferWildcardCert &&
|
||||
!domain.startsWith("*.")
|
||||
) {
|
||||
// in this case traefik is going to generate a domain one level down so we need to store it that way
|
||||
const parts = domain.split(".");
|
||||
if (parts.length > 2) {
|
||||
domainToWrite = parts.slice(1).join(".");
|
||||
domainToWrite = `*.${domainToWrite}`;
|
||||
}
|
||||
} else if (domainRecord.type == "ns") {
|
||||
if (domain == domainRecord.baseDomain) {
|
||||
domainToWrite = domainRecord.baseDomain;
|
||||
} else {
|
||||
const parts = domain.split(".");
|
||||
if (parts.length > 2) {
|
||||
domainToWrite = parts.slice(1).join(".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No cert found, create a new one in pending state
|
||||
await trx.insert(certificates).values({
|
||||
domain: domainToWrite,
|
||||
domainId,
|
||||
wildcard:
|
||||
domainRecord.type == "ns" ||
|
||||
(domainRecord.type == "wildcard" &&
|
||||
domainRecord.preferWildcardCert), // we can only create wildcard certs for NS domains
|
||||
status: "pending",
|
||||
updatedAt: Math.floor(Date.now() / 1000),
|
||||
createdAt: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
}
|
||||
|
||||
-12
@@ -1,15 +1,3 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
import { certificates, db, domainNamespaces, domains, orgDomains } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
-13
@@ -1,16 +1,3 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { certificates, db, domains } from "@server/db";
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./getCertificate";
|
||||
export * from "./restartCertificate";
|
||||
export * from "./syncCertToNewts";
|
||||
export * from "./getBatchedCertificates";
|
||||
export * from "./createCertificate";
|
||||
-13
@@ -1,16 +1,3 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { certificates, db } from "@server/db";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
+2
-15
@@ -1,19 +1,6 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { z } from "zod";
|
||||
import { pushCertUpdateToAffectedNewts } from "#private/lib/acmeCertSync";
|
||||
import { pushCertUpdateToAffectedNewts } from "@server/lib/acmeCertSync";
|
||||
import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import createHttpError from "http-errors";
|
||||
@@ -65,4 +52,4 @@ export async function syncCertToNewts(
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,20 +50,21 @@ import {
|
||||
verifyAiProviderAccess,
|
||||
verifyAiModelAccess,
|
||||
verifyAiBudgetAccess,
|
||||
verifyVirtualApiKeyAccess
|
||||
verifyVirtualApiKeyAccess,
|
||||
logActionAudit,
|
||||
verifyCertificateAccess
|
||||
} from "@server/middlewares";
|
||||
import { ActionsEnum } from "@server/auth/actions";
|
||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||
import createHttpError from "http-errors";
|
||||
import { build } from "@server/build";
|
||||
import { createStore } from "#dynamic/lib/rateLimitStore";
|
||||
import { logActionAudit, verifyValidLicense } from "#dynamic/middlewares";
|
||||
import { checkRoundTripMessage } from "./ws";
|
||||
import * as labels from "@server/routers/labels";
|
||||
import * as aiProvider from "@server/routers/aiProvider";
|
||||
import * as aiBudget from "@server/routers/aiBudget";
|
||||
import * as virtualApiKey from "@server/routers/virtualApiKey";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
import * as certificates from "@server/routers/certificates";
|
||||
|
||||
// Root routes
|
||||
export const unauthenticated = Router();
|
||||
@@ -1858,7 +1859,6 @@ authenticated.put(
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/ssh/sign-key",
|
||||
verifyValidLicense,
|
||||
verifyOrgAccess,
|
||||
verifyLimits,
|
||||
// verifyUserHasAction(ActionsEnum.signSshKey), // this check happens inside of the function now
|
||||
@@ -1878,6 +1878,31 @@ authenticated.post(
|
||||
client.rebuildClientAssociationsCacheRoute
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/certificate/:domainId/:domain",
|
||||
verifyOrgAccess,
|
||||
verifyCertificateAccess,
|
||||
verifyUserHasAction(ActionsEnum.getCertificate),
|
||||
certificates.getCertificate
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/org/:orgId/batched-certificates",
|
||||
verifyOrgAccess,
|
||||
verifyUserHasAction(ActionsEnum.getCertificate),
|
||||
certificates.getBatchedCertificates
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/org/:orgId/certificate/:certId/restart",
|
||||
verifyOrgAccess,
|
||||
verifyCertificateAccess,
|
||||
verifyLimits,
|
||||
verifyUserHasAction(ActionsEnum.restartCertificate),
|
||||
logActionAudit(ActionsEnum.restartCertificate),
|
||||
certificates.restartCertificate
|
||||
);
|
||||
|
||||
// Auth routes
|
||||
export const authRouter = Router();
|
||||
unauthenticated.use("/auth", authRouter);
|
||||
|
||||
@@ -24,7 +24,7 @@ import logger from "@server/logger";
|
||||
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
|
||||
import config from "@server/lib/config";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { createCertificate } from "@server/routers/certificates";
|
||||
import {
|
||||
validateAndConstructDomain,
|
||||
checkWildcardDomainConflict
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
} from "@server/lib/schemas";
|
||||
import { registry } from "@server/openApi";
|
||||
import { OpenAPITags } from "@server/openApi";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { createCertificate } from "@server/routers/certificates/createCertificate";
|
||||
import {
|
||||
validateAndConstructDomain,
|
||||
checkWildcardDomainConflict
|
||||
|
||||
@@ -32,7 +32,7 @@ import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
||||
import { createCertificate } from "@server/routers/certificates/createCertificate";
|
||||
import { build } from "@server/build";
|
||||
import { usageService } from "@server/lib/billing/usageService";
|
||||
import { LimitId } from "@server/lib/billing";
|
||||
|
||||
Reference in New Issue
Block a user