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
|
labels
|
||||||
} from "./schema";
|
} 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", {
|
export const dnsChallenge = pgTable("dnsChallenges", {
|
||||||
dnsChallengeId: serial("dnsChallengeId").primaryKey(),
|
dnsChallengeId: serial("dnsChallengeId").primaryKey(),
|
||||||
domain: varchar("domain", { length: 255 }).notNull(),
|
domain: varchar("domain", { length: 255 }).notNull(),
|
||||||
@@ -633,7 +614,6 @@ export const trialNotifications = pgTable("trialNotifications", {
|
|||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
|
||||||
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
||||||
export type Customer = InferSelectModel<typeof customers>;
|
export type Customer = InferSelectModel<typeof customers>;
|
||||||
export type Subscription = InferSelectModel<typeof subscriptions>;
|
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 Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -2117,3 +2136,4 @@ export type SiteResourceAiProvider = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type ResourceAiModel = InferSelectModel<typeof resourceAiModels>;
|
export type ResourceAiModel = InferSelectModel<typeof resourceAiModels>;
|
||||||
export type SiteResourceAiModel = InferSelectModel<typeof siteResourceAiModels>;
|
export type SiteResourceAiModel = InferSelectModel<typeof siteResourceAiModels>;
|
||||||
|
export type Certificate = InferSelectModel<typeof certificates>;
|
||||||
|
|||||||
@@ -23,25 +23,6 @@ import {
|
|||||||
users
|
users
|
||||||
} from "./schema";
|
} 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", {
|
export const dnsChallenge = sqliteTable("dnsChallenges", {
|
||||||
dnsChallengeId: integer("dnsChallengeId").primaryKey({
|
dnsChallengeId: integer("dnsChallengeId").primaryKey({
|
||||||
autoIncrement: true
|
autoIncrement: true
|
||||||
@@ -628,7 +609,6 @@ export const trialNotifications = sqliteTable("trialNotifications", {
|
|||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
|
||||||
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
|
||||||
export type Customer = InferSelectModel<typeof customers>;
|
export type Customer = InferSelectModel<typeof customers>;
|
||||||
export type Subscription = InferSelectModel<typeof subscriptions>;
|
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 Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -2111,3 +2130,4 @@ export type SiteResourceAiProvider = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type ResourceAiModel = InferSelectModel<typeof resourceAiModels>;
|
export type ResourceAiModel = InferSelectModel<typeof resourceAiModels>;
|
||||||
export type SiteResourceAiModel = InferSelectModel<typeof siteResourceAiModels>;
|
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 { initCleanup } from "#dynamic/cleanup";
|
||||||
import license from "#dynamic/license/license";
|
import license from "#dynamic/license/license";
|
||||||
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
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 { fetchServerIp } from "@server/lib/serverIpService";
|
||||||
import { startRebuildQueueProcessor } from "@server/lib/rebuildClientAssociations";
|
import { startRebuildQueueProcessor } from "@server/lib/rebuildClientAssociations";
|
||||||
import { initAiModelCatalog } from "@server/lib/aiModelCatalog";
|
import { initAiModelCatalog } from "@server/lib/aiModelCatalog";
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { getOrCreateLabelIds, syncSiteResourceLabels } from "./labels";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
||||||
import { getNextAvailableAliasAddress } from "../ip";
|
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 { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { tierMatrix } from "../billing/tierMatrix";
|
import { tierMatrix } from "../billing/tierMatrix";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
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 { hashPassword } from "@server/auth/password";
|
||||||
import { generateId } from "@server/auth/sessions/app";
|
import { generateId } from "@server/auth/sessions/app";
|
||||||
import { build } from "@server/build";
|
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(
|
export async function getValidCertificatesForDomains(
|
||||||
domains: Set<string>,
|
domains: Set<string>,
|
||||||
useCache: boolean = true
|
useCache: boolean = true
|
||||||
): Promise<
|
): Promise<Array<CertificateResult>> {
|
||||||
Array<{
|
const finalResults: CertificateResult[] = [];
|
||||||
id: number;
|
const domainsToQuery = new Set<string>();
|
||||||
domain: string;
|
|
||||||
queriedDomain: string;
|
// 1. Check cache first if enabled
|
||||||
wildcard: boolean | null;
|
if (useCache) {
|
||||||
certFile: string | null;
|
for (const domain of domains) {
|
||||||
keyFile: string | null;
|
const cacheKey = `cert:${domain}`;
|
||||||
expiresAt: number | null;
|
const cachedCert = await cache.get<CertificateResult>(cacheKey);
|
||||||
updatedAt?: number | null;
|
if (cachedCert) {
|
||||||
}>
|
finalResults.push(cachedCert); // Valid cache hit
|
||||||
> {
|
} else {
|
||||||
return []; // stub
|
// 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 logger from "@server/logger";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { createHash } from "crypto";
|
import { createHash } from "crypto";
|
||||||
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
import { getValidCertificatesForDomains } from "@server/lib/certificates";
|
||||||
import { lockManager } from "#dynamic/lib/lock";
|
import { lockManager } from "#dynamic/lib/lock";
|
||||||
|
|
||||||
interface IPRange {
|
interface IPRange {
|
||||||
|
|||||||
@@ -8,7 +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 { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
import { getValidCertificatesForDomains } from "@server/lib/certificates";
|
||||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
@@ -628,8 +628,7 @@ export class TraefikConfigManager {
|
|||||||
.name,
|
.name,
|
||||||
|
|
||||||
remoteRoleHeader:
|
remoteRoleHeader:
|
||||||
config.getRawConfig().server.remote_headers
|
config.getRawConfig().server.remote_headers.role
|
||||||
.role
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -38,3 +38,4 @@ export * from "./logActionAudit";
|
|||||||
export * from "./verifyOlmAccess";
|
export * from "./verifyOlmAccess";
|
||||||
export * from "./verifyLimits";
|
export * from "./verifyLimits";
|
||||||
export * from "./verifyResourcePolicyAccess";
|
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 { Request, Response, NextFunction } from "express";
|
||||||
import { db, domainNamespaces } from "@server/db";
|
import { db, domainNamespaces } from "@server/db";
|
||||||
import { certificates } 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 {
|
import {
|
||||||
CertificateResult,
|
CertificateResult,
|
||||||
getValidCertificatesForDomains
|
getValidCertificatesForDomains
|
||||||
} from "#private/lib/certificates";
|
} from "@server/lib/certificates";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import regionalCache from "#private/lib/cache";
|
import regionalCache from "#private/lib/cache";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
* This file is not licensed under the AGPLv3.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "./verifyCertificateAccess";
|
|
||||||
export * from "./verifyRemoteExitNodeAccess";
|
export * from "./verifyRemoteExitNodeAccess";
|
||||||
export * from "./verifyIdpAccess";
|
export * from "./verifyIdpAccess";
|
||||||
export * from "./verifyLoginPageAccess";
|
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.
|
* This file is not licensed under the AGPLv3.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as certificates from "#private/routers/certificates";
|
|
||||||
import { createStore } from "#private/lib/rateLimitStore";
|
import { createStore } from "#private/lib/rateLimitStore";
|
||||||
import * as billing from "#private/routers/billing";
|
import * as billing from "#private/routers/billing";
|
||||||
import * as remoteExitNode from "#private/routers/remoteExitNode";
|
import * as remoteExitNode from "#private/routers/remoteExitNode";
|
||||||
@@ -50,7 +49,6 @@ import {
|
|||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
import {
|
import {
|
||||||
logActionAudit,
|
logActionAudit,
|
||||||
verifyCertificateAccess,
|
|
||||||
verifyIdpAccess,
|
verifyIdpAccess,
|
||||||
verifyLoginPageAccess,
|
verifyLoginPageAccess,
|
||||||
verifyRemoteExitNodeAccess,
|
verifyRemoteExitNodeAccess,
|
||||||
@@ -164,32 +162,6 @@ authenticated.get(
|
|||||||
orgIdp.listUserAdminOrgIdps
|
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") {
|
if (build === "saas") {
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/org/:orgId/billing/create-checkout-session",
|
"/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 org from "#private/routers/org";
|
||||||
import * as logs from "#private/routers/auditLogs";
|
import * as logs from "#private/routers/auditLogs";
|
||||||
import * as alertEvents from "#private/routers/alertEvents";
|
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 siteProvisioning from "#private/routers/siteProvisioning";
|
||||||
import * as policy from "#private/routers/policy";
|
import * as policy from "#private/routers/policy";
|
||||||
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import logger from "@server/logger";
|
|||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
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";
|
import { CreateLoginPageResponse } from "@server/routers/loginPage/types";
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
||||||
import { subdomainSchema } from "@server/lib/schemas";
|
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";
|
import { UpdateLoginPageResponse } from "@server/routers/loginPage/types";
|
||||||
|
|
||||||
@@ -85,7 +85,6 @@ export async function updateLoginPage(
|
|||||||
|
|
||||||
const { loginPageId, orgId } = parsedParams.data;
|
const { loginPageId, orgId } = parsedParams.data;
|
||||||
|
|
||||||
|
|
||||||
const [existingLoginPage] = await db
|
const [existingLoginPage] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(loginPage)
|
.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(
|
export async function createCertificate(
|
||||||
domainId: string,
|
domainId: string,
|
||||||
domain: string,
|
domain: string,
|
||||||
trx: Transaction | typeof db
|
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 { certificates, db, domainNamespaces, domains, orgDomains } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
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 { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { certificates, db, domains } from "@server/db";
|
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 { certificates, db } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import logger from "@server/logger";
|
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 { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { pushCertUpdateToAffectedNewts } from "#private/lib/acmeCertSync";
|
import { pushCertUpdateToAffectedNewts } from "@server/lib/acmeCertSync";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -65,4 +52,4 @@ export async function syncCertToNewts(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,20 +50,21 @@ import {
|
|||||||
verifyAiProviderAccess,
|
verifyAiProviderAccess,
|
||||||
verifyAiModelAccess,
|
verifyAiModelAccess,
|
||||||
verifyAiBudgetAccess,
|
verifyAiBudgetAccess,
|
||||||
verifyVirtualApiKeyAccess
|
verifyVirtualApiKeyAccess,
|
||||||
|
logActionAudit,
|
||||||
|
verifyCertificateAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { createStore } from "#dynamic/lib/rateLimitStore";
|
import { createStore } from "#dynamic/lib/rateLimitStore";
|
||||||
import { logActionAudit, verifyValidLicense } from "#dynamic/middlewares";
|
|
||||||
import { checkRoundTripMessage } from "./ws";
|
import { checkRoundTripMessage } from "./ws";
|
||||||
import * as labels from "@server/routers/labels";
|
import * as labels from "@server/routers/labels";
|
||||||
import * as aiProvider from "@server/routers/aiProvider";
|
import * as aiProvider from "@server/routers/aiProvider";
|
||||||
import * as aiBudget from "@server/routers/aiBudget";
|
import * as aiBudget from "@server/routers/aiBudget";
|
||||||
import * as virtualApiKey from "@server/routers/virtualApiKey";
|
import * as virtualApiKey from "@server/routers/virtualApiKey";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import * as certificates from "@server/routers/certificates";
|
||||||
|
|
||||||
// Root routes
|
// Root routes
|
||||||
export const unauthenticated = Router();
|
export const unauthenticated = Router();
|
||||||
@@ -1858,7 +1859,6 @@ authenticated.put(
|
|||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/org/:orgId/ssh/sign-key",
|
"/org/:orgId/ssh/sign-key",
|
||||||
verifyValidLicense,
|
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
verifyLimits,
|
verifyLimits,
|
||||||
// verifyUserHasAction(ActionsEnum.signSshKey), // this check happens inside of the function now
|
// verifyUserHasAction(ActionsEnum.signSshKey), // this check happens inside of the function now
|
||||||
@@ -1878,6 +1878,31 @@ authenticated.post(
|
|||||||
client.rebuildClientAssociationsCacheRoute
|
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
|
// Auth routes
|
||||||
export const authRouter = Router();
|
export const authRouter = Router();
|
||||||
unauthenticated.use("/auth", authRouter);
|
unauthenticated.use("/auth", authRouter);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import logger from "@server/logger";
|
|||||||
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
|
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "@server/routers/certificates";
|
||||||
import {
|
import {
|
||||||
validateAndConstructDomain,
|
validateAndConstructDomain,
|
||||||
checkWildcardDomainConflict
|
checkWildcardDomainConflict
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
} from "@server/lib/schemas";
|
} from "@server/lib/schemas";
|
||||||
import { registry } from "@server/openApi";
|
import { registry } from "@server/openApi";
|
||||||
import { OpenAPITags } from "@server/openApi";
|
import { OpenAPITags } from "@server/openApi";
|
||||||
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
|
import { createCertificate } from "@server/routers/certificates/createCertificate";
|
||||||
import {
|
import {
|
||||||
validateAndConstructDomain,
|
validateAndConstructDomain,
|
||||||
checkWildcardDomainConflict
|
checkWildcardDomainConflict
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import createHttpError from "http-errors";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { validateAndConstructDomain } from "@server/lib/domainUtils";
|
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 { build } from "@server/build";
|
||||||
import { usageService } from "@server/lib/billing/usageService";
|
import { usageService } from "@server/lib/billing/usageService";
|
||||||
import { LimitId } from "@server/lib/billing";
|
import { LimitId } from "@server/lib/billing";
|
||||||
|
|||||||
Reference in New Issue
Block a user