Files
MikrotikManager/backend/src/services/certificates-service.ts
T

171 lines
5.4 KiB
TypeScript

import { eq } from "drizzle-orm"
import type { CertificateDto } from "@mmapp/contracts/certificates"
import { db } from "../db/index.js"
import { acmeSettings, certificateIssueJobs, servers } from "../db/schema.js"
import { mapRosCertificates } from "./certificate-parse.js"
import { MikrotikClient } from "./mikrotik.js"
const SETTINGS_ID = 1
export async function listCertificatesFromServers(): Promise<{
certificates: CertificateDto[]
failures: Array<{ serverId: string; serverName?: string; error: string }>
}> {
const allServers = db.select().from(servers).where(eq(servers.enabled, true)).all()
const certificates: CertificateDto[] = []
const failures: Array<{ serverId: string; serverName?: string; error: string }> = []
await Promise.all(
allServers.map(async (server) => {
try {
const client = MikrotikClient.fromServer(server)
const rows = await client.getCertificates()
certificates.push(...mapRosCertificates(server.id, server.name, rows))
} catch (e) {
failures.push({
serverId: String(server.id),
serverName: server.name,
error: e instanceof Error ? e.message : String(e),
})
}
}),
)
certificates.sort((a, b) => {
if (a.serverId !== b.serverId) return a.serverId.localeCompare(b.serverId)
return a.name.localeCompare(b.name, "ru")
})
return { certificates, failures }
}
export function getAcmeSettingsPublic() {
const row = db.select().from(acmeSettings).where(eq(acmeSettings.id, SETTINGS_ID)).limit(1).all()[0]
if (!row) {
return {
directoryUrl: "https://acme-v02.api.letsencrypt.org/directory",
defaultZoneId: "",
tokenConfigured: false,
updatedAt: undefined as string | undefined,
}
}
return {
directoryUrl: row.directoryUrl,
defaultZoneId: row.defaultZoneId || undefined,
tokenConfigured: Boolean(row.cloudflareApiToken?.trim()),
updatedAt: row.updatedAt,
}
}
export function getAcmeCloudflareToken(): string {
const row = db.select().from(acmeSettings).where(eq(acmeSettings.id, SETTINGS_ID)).limit(1).all()[0]
return row?.cloudflareApiToken?.trim() ?? ""
}
export function getAcmeAccountPrivateKey(): string {
const row = db.select().from(acmeSettings).where(eq(acmeSettings.id, SETTINGS_ID)).limit(1).all()[0]
return row?.accountPrivateKey?.trim() ?? ""
}
export function saveAcmeAccountPrivateKey(pem: string) {
const now = new Date().toISOString()
db.update(acmeSettings)
.set({ accountPrivateKey: pem, updatedAt: now })
.where(eq(acmeSettings.id, SETTINGS_ID))
.run()
}
export function updateAcmeSettings(input: {
directoryUrl?: string
defaultZoneId?: string | null
cloudflareApiToken?: string | null
}) {
const row = db.select().from(acmeSettings).where(eq(acmeSettings.id, SETTINGS_ID)).limit(1).all()[0]
const now = new Date().toISOString()
const next = {
directoryUrl: input.directoryUrl?.trim() || row?.directoryUrl || "https://acme-v02.api.letsencrypt.org/directory",
defaultZoneId:
input.defaultZoneId === null
? ""
: input.defaultZoneId?.trim() ?? row?.defaultZoneId ?? "",
cloudflareApiToken:
input.cloudflareApiToken === null
? ""
: input.cloudflareApiToken?.trim() ?? row?.cloudflareApiToken ?? "",
updatedAt: now,
}
if (row) {
db.update(acmeSettings).set(next).where(eq(acmeSettings.id, SETTINGS_ID)).run()
} else {
db.insert(acmeSettings).values({ id: SETTINGS_ID, accountPrivateKey: "", ...next }).run()
}
return getAcmeSettingsPublic()
}
export function createIssueJobRecord(input: {
id: string
serverId: string
certName: string
domainNames: string[]
keyType: string
trustStore: string
}) {
const now = new Date().toISOString()
db.insert(certificateIssueJobs).values({
id: input.id,
status: "queued",
step: "queued",
serverId: input.serverId,
certName: input.certName,
domainNames: JSON.stringify(input.domainNames),
keyType: input.keyType,
trustStore: input.trustStore,
requestedAt: now,
}).run()
}
export function updateIssueJobRecord(
id: string,
patch: Partial<{
status: "queued" | "running" | "done" | "failed"
step: string
startedAt: string
finishedAt: string
error: string | null
}>,
) {
db.update(certificateIssueJobs).set(patch).where(eq(certificateIssueJobs.id, id)).run()
}
export function getIssueJobRecord(id: string) {
return db.select().from(certificateIssueJobs).where(eq(certificateIssueJobs.id, id)).limit(1).all()[0] ?? null
}
export function toIssueJobDto(row: NonNullable<ReturnType<typeof getIssueJobRecord>>) {
let domainNames: string[] = []
try {
const parsed = JSON.parse(row.domainNames) as unknown
if (Array.isArray(parsed)) domainNames = parsed.map(String)
} catch {
domainNames = []
}
return {
id: row.id,
status: row.status,
step: row.step as "queued" | "acme_order" | "dns_challenge" | "finalize" | "import" | "cleanup" | "done",
serverId: row.serverId,
certName: row.certName,
domainNames,
requestedAt: row.requestedAt,
startedAt: row.startedAt ?? undefined,
finishedAt: row.finishedAt ?? undefined,
error: row.error ?? undefined,
}
}
export function getServerRowByIdString(serverId: string) {
const id = Number.parseInt(serverId, 10)
if (!Number.isFinite(id)) return null
return db.select().from(servers).where(eq(servers.id, id)).limit(1).all()[0] ?? null
}