feat(certificates): enhance service certificate management and monitoring
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 8s
quality / changes (push) Successful in 10s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m8s
quality / api (push) Successful in 1m9s
CD / quality (push) Successful in 2m31s
CD / publish (push) Successful in 1m35s

- Added new endpoints for listing and checking service certificates, improving visibility into SSL status.
- Integrated certificate monitoring options into service binding updates, allowing for flexible SSL management.
- Updated the service detail grid to include SSL monitoring controls, enhancing user interaction with certificate settings.
- Refactored related components and schemas to support the new certificate features, ensuring consistency across the application.
- Improved test coverage for certificate functionalities, validating the new features and ensuring reliability.
This commit is contained in:
Denozordec
2026-08-20 00:32:43 +07:00
parent 69119a08a4
commit a458465153
20 changed files with 760 additions and 274 deletions
@@ -0,0 +1,37 @@
ALTER TABLE service_bindings ADD COLUMN cert_monitoring TEXT NOT NULL DEFAULT 'auto'
CHECK (cert_monitoring IN ('auto', 'required', 'skipped'));
ALTER TABLE certificates ADD COLUMN service_id INTEGER REFERENCES services(id) ON DELETE SET NULL;
UPDATE service_bindings
SET cert_monitoring = COALESCE(
(
SELECT d.cert_monitoring FROM domains d
WHERE d.id = service_bindings.domain_id
),
'auto'
)
WHERE hostname = '@';
UPDATE service_bindings
SET cert_monitoring = COALESCE(
(
SELECT s.cert_monitoring FROM subdomains s
WHERE s.domain_id = service_bindings.domain_id
AND s.name = service_bindings.hostname
),
'auto'
)
WHERE hostname != '@';
UPDATE certificates
SET service_id = (
SELECT sb.service_id
FROM service_bindings sb
JOIN domains d ON d.id = sb.domain_id
WHERE CASE
WHEN sb.hostname = '@' THEN d.zone_name
ELSE sb.hostname || '.' || d.zone_name
END = certificates.hostname
LIMIT 1
);
+67 -16
View File
@@ -851,6 +851,7 @@ function mapServiceBinding(
health_check_timeout_ms: row.health_check_timeout_ms,
health_check_verify_tls: row.health_check_verify_tls,
...mapHealthFields(row),
cert_monitoring: row.cert_monitoring ?? "auto",
routing_strategy: row.routing_strategy as LbMode,
operation_version: row.operation_version,
created_at: row.created_at,
@@ -1530,6 +1531,7 @@ export interface BindingLbPatch {
health_check_provider?: HealthCheckProvider;
health_check_providers?: HealthCheckProvider[];
health_check_aggregate?: HealthCheckAggregate;
cert_monitoring?: string;
}
export function updateBindingLbConfig(
@@ -1560,6 +1562,8 @@ export function updateBindingLbConfig(
update.health_check_timeout_ms = patch.health_check_timeout_ms;
if (patch.health_check_verify_tls !== undefined)
update.health_check_verify_tls = patch.health_check_verify_tls;
if (patch.cert_monitoring !== undefined)
update.cert_monitoring = patch.cert_monitoring;
Object.assign(update, healthProviderColumns(patch));
db.update(serviceBindings)
.set(update)
@@ -1669,7 +1673,7 @@ const SERVICE_BINDING_SELECT_COLUMNS = `sb.id, sb.domain_id, sb.service_id, sb.h
sb.lb_mode, sb.health_check_enabled, sb.health_check_type, sb.health_check_port,
sb.health_check_path, sb.health_check_expected_status, sb.health_check_interval_sec,
sb.health_check_timeout_ms, sb.health_check_verify_tls, sb.health_check_provider,
sb.health_check_providers, sb.health_check_aggregate, sb.cname_target,
sb.health_check_providers, sb.health_check_aggregate, sb.cert_monitoring, sb.cname_target,
d.zone_name, d.group_id, g.name AS group_name,
s.name AS service_name, s.slug AS service_slug,
dr.content AS target_ip, dr.sync_status,
@@ -1733,6 +1737,7 @@ function enrichServiceBindingView(
return {
...row,
cname_target: row.cname_target ?? null,
cert_monitoring: row.cert_monitoring ?? "auto",
...mapHealthFields(row),
target_ips,
target_ip: target_ips[0] ?? null,
@@ -1914,26 +1919,69 @@ export function deleteBinding(db: Db, id: number): void {
// --- Certificates ---
const CERTIFICATE_SELECT = `c.id, c.domain_id, c.subdomain_id, c.service_id, c.hostname,
c.expires_at, c.last_checked_at, c.last_error, c.status, c.created_at, c.updated_at,
s.name AS service_name`;
type CertificateRow = {
id: number;
domain_id: number;
subdomain_id: number | null;
service_id: number | null;
hostname: string;
expires_at: string | null;
last_checked_at: string | null;
last_error: string | null;
status: string;
created_at: string;
updated_at: string;
service_name: string | null;
};
function mapCertificate(row: CertificateRow): Certificate {
return {
id: row.id,
domain_id: row.domain_id,
subdomain_id: row.subdomain_id,
service_id: row.service_id ?? null,
service_name: row.service_name ?? null,
hostname: row.hostname,
expires_at: row.expires_at,
last_checked_at: row.last_checked_at,
last_error: row.last_error,
status: row.status,
created_at: row.created_at,
updated_at: row.updated_at,
};
}
export function listCertificates(db: Db, status?: string): Certificate[] {
if (status) {
return db
.select()
.from(certificates)
.where(eq(certificates.status, status))
.orderBy(asc(certificates.expires_at))
.all() as Certificate[];
}
return db
.select()
.from(certificates)
.orderBy(asc(certificates.expires_at))
.all() as Certificate[];
const rows = status
? db.all<CertificateRow>(sql`
SELECT ${sql.raw(CERTIFICATE_SELECT)}
FROM certificates c
LEFT JOIN services s ON s.id = c.service_id
WHERE c.status = ${status}
ORDER BY c.expires_at ASC
`)
: db.all<CertificateRow>(sql`
SELECT ${sql.raw(CERTIFICATE_SELECT)}
FROM certificates c
LEFT JOIN services s ON s.id = c.service_id
ORDER BY c.expires_at ASC
`);
return rows.map(mapCertificate);
}
export function getCertificate(db: Db, id: number): Certificate {
const row = db.select().from(certificates).where(eq(certificates.id, id)).get();
const row = db.all<CertificateRow>(sql`
SELECT ${sql.raw(CERTIFICATE_SELECT)}
FROM certificates c
LEFT JOIN services s ON s.id = c.service_id
WHERE c.id = ${id}
`)[0];
if (!row) throw new NotFoundError(`certificate ${id}`);
return row as Certificate;
return mapCertificate(row);
}
export function upsertCertificateCheck(
@@ -1944,6 +1992,7 @@ export function upsertCertificateCheck(
expiresAt: string | null,
status: string,
lastError: string | null,
serviceId?: number | null,
): Certificate {
const existing = db
.select()
@@ -1956,6 +2005,7 @@ export function upsertCertificateCheck(
.set({
domain_id: domainId,
subdomain_id: subdomainId,
service_id: serviceId === undefined ? existing.service_id : serviceId,
expires_at: expiresAt,
last_checked_at: sql`datetime('now')`,
last_error: lastError,
@@ -1972,6 +2022,7 @@ export function upsertCertificateCheck(
.values({
domain_id: domainId,
subdomain_id: subdomainId,
service_id: serviceId ?? null,
hostname,
expires_at: expiresAt,
last_checked_at: sql`datetime('now')`,
+4
View File
@@ -177,6 +177,7 @@ export const serviceBindings = sqliteTable(
health_check_aggregate: text("health_check_aggregate")
.notNull()
.default("majority"),
cert_monitoring: text("cert_monitoring").notNull().default("auto"),
routing_strategy: text("routing_strategy").notNull().default("round_robin"),
operation_version: integer("operation_version").notNull().default(0),
created_at: text("created_at")
@@ -324,6 +325,9 @@ export const certificates = sqliteTable("certificates", {
subdomain_id: integer("subdomain_id").references(() => subdomains.id, {
onDelete: "set null",
}),
service_id: integer("service_id").references(() => services.id, {
onDelete: "set null",
}),
hostname: text("hostname").notNull().unique(),
expires_at: text("expires_at"),
last_checked_at: text("last_checked_at"),