171 lines
5.4 KiB
TypeScript
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
|
|
}
|