148 lines
4.4 KiB
TypeScript
148 lines
4.4 KiB
TypeScript
import db, { nowIso, toIso } from "../db";
|
|
import { logAuditEvent } from "../audit";
|
|
import { applyCaddyConfig } from "../caddy";
|
|
import { caCertificates, proxyHosts } from "../db/schema";
|
|
import { desc, eq } from "drizzle-orm";
|
|
|
|
function tryParseJson<T>(value: string | null | undefined, fallback: T): T {
|
|
if (!value) return fallback;
|
|
try {
|
|
return JSON.parse(value) as T;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
export type CaCertificate = {
|
|
id: number;
|
|
name: string;
|
|
certificate_pem: string;
|
|
has_private_key: boolean;
|
|
created_at: string;
|
|
updated_at: string;
|
|
};
|
|
|
|
export type CaCertificateInput = {
|
|
name: string;
|
|
certificate_pem: string;
|
|
private_key_pem?: string;
|
|
};
|
|
|
|
type CaCertificateRow = typeof caCertificates.$inferSelect;
|
|
|
|
function parseCaCertificate(row: CaCertificateRow): CaCertificate {
|
|
return {
|
|
id: row.id,
|
|
name: row.name,
|
|
certificate_pem: row.certificatePem,
|
|
has_private_key: !!row.privateKeyPem,
|
|
created_at: toIso(row.createdAt)!,
|
|
updated_at: toIso(row.updatedAt)!
|
|
};
|
|
}
|
|
|
|
export async function listCaCertificates(): Promise<CaCertificate[]> {
|
|
const rows = await db.select().from(caCertificates).orderBy(desc(caCertificates.createdAt));
|
|
return rows.map(parseCaCertificate);
|
|
}
|
|
|
|
export async function getCaCertificatePrivateKey(id: number): Promise<string | null> {
|
|
const cert = await db.query.caCertificates.findFirst({
|
|
where: (table, { eq }) => eq(table.id, id)
|
|
});
|
|
return cert?.privateKeyPem ?? null;
|
|
}
|
|
|
|
export async function getCaCertificate(id: number): Promise<CaCertificate | null> {
|
|
const cert = await db.query.caCertificates.findFirst({
|
|
where: (table, { eq }) => eq(table.id, id)
|
|
});
|
|
return cert ? parseCaCertificate(cert) : null;
|
|
}
|
|
|
|
export async function createCaCertificate(input: CaCertificateInput, actorUserId: number): Promise<CaCertificate> {
|
|
const now = nowIso();
|
|
const [record] = await db
|
|
.insert(caCertificates)
|
|
.values({
|
|
name: input.name.trim(),
|
|
certificatePem: input.certificate_pem.trim(),
|
|
privateKeyPem: input.private_key_pem?.trim() ?? null,
|
|
createdBy: actorUserId,
|
|
createdAt: now,
|
|
updatedAt: now
|
|
})
|
|
.returning();
|
|
|
|
if (!record) {
|
|
throw new Error("Failed to create CA certificate");
|
|
}
|
|
|
|
logAuditEvent({
|
|
userId: actorUserId,
|
|
action: "create",
|
|
entityType: "ca_certificate",
|
|
entityId: record.id,
|
|
summary: `Created CA certificate ${input.name}`
|
|
});
|
|
await applyCaddyConfig();
|
|
return (await getCaCertificate(record.id))!;
|
|
}
|
|
|
|
export async function updateCaCertificate(id: number, input: Partial<CaCertificateInput>, actorUserId: number): Promise<CaCertificate> {
|
|
const existing = await getCaCertificate(id);
|
|
if (!existing) {
|
|
throw new Error("CA certificate not found");
|
|
}
|
|
|
|
const now = nowIso();
|
|
await db
|
|
.update(caCertificates)
|
|
.set({
|
|
name: input.name?.trim() ?? existing.name,
|
|
certificatePem: input.certificate_pem?.trim() ?? existing.certificate_pem,
|
|
...(input.private_key_pem !== undefined ? { privateKeyPem: input.private_key_pem?.trim() ?? null } : {}),
|
|
updatedAt: now
|
|
})
|
|
.where(eq(caCertificates.id, id));
|
|
|
|
logAuditEvent({
|
|
userId: actorUserId,
|
|
action: "update",
|
|
entityType: "ca_certificate",
|
|
entityId: id,
|
|
summary: `Updated CA certificate ${input.name ?? existing.name}`
|
|
});
|
|
await applyCaddyConfig();
|
|
return (await getCaCertificate(id))!;
|
|
}
|
|
|
|
export async function deleteCaCertificate(id: number, actorUserId: number): Promise<void> {
|
|
const existing = await getCaCertificate(id);
|
|
if (!existing) {
|
|
throw new Error("CA certificate not found");
|
|
}
|
|
|
|
// Check if any proxy hosts reference this CA cert
|
|
const allHosts = await db.select({ meta: proxyHosts.meta, name: proxyHosts.name }).from(proxyHosts);
|
|
const referencing = allHosts.filter((host) => {
|
|
const meta = tryParseJson<{ mtls?: { enabled?: boolean; ca_certificate_ids?: number[] } }>(host.meta, {});
|
|
return meta.mtls?.enabled && meta.mtls.ca_certificate_ids?.includes(id);
|
|
});
|
|
|
|
if (referencing.length > 0) {
|
|
const names = referencing.map((h) => h.name).join(", ");
|
|
throw new Error(`CA certificate is in use by proxy host(s): ${names}`);
|
|
}
|
|
|
|
await db.delete(caCertificates).where(eq(caCertificates.id, id));
|
|
logAuditEvent({
|
|
userId: actorUserId,
|
|
action: "delete",
|
|
entityType: "ca_certificate",
|
|
entityId: id,
|
|
summary: `Deleted CA certificate ${existing.name}`
|
|
});
|
|
await applyCaddyConfig();
|
|
}
|