Files
MikrotikManager/backend/src/routes/certificates.ts
T
DenozordecandCursor ec43591a99
Docker images / prepare-release (push) Successful in 12s
Docker images / backend-test (push) Successful in 4m9s
Docker images / frontend-image (push) Successful in 4m28s
Docker images / updater-image (push) Successful in 58s
Docker images / backend-image (push) Successful in 2m49s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 11s
feat(db): перевести хранилище с SQLite на PostgreSQL
При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump.

Co-authored-by: Cursor <[email protected]>
2026-09-08 01:36:48 +07:00

113 lines
4.3 KiB
TypeScript

import { randomUUID } from "node:crypto"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import {
certificateIssueRequestSchema,
putAcmeCloudflareSettingsSchema,
putCertificateRenewSettingsSchema,
testAcmeCloudflareSettingsSchema,
} from "@mmapp/contracts/certificates"
import { refreshScheduler } from "../services/scheduler.js"
import { testCloudflareToken } from "../services/acme-cloudflare.js"
import { queueCertificateIssueJob } from "../services/certificate-issue-runner.js"
import {
createIssueJobRecord,
getAcmeCloudflareToken,
getAcmeSettingsPublic,
getCertificateRenewSettings,
getIssueJobRecord,
getServerRowByIdString,
listCertificatesFromServers,
toIssueJobDto,
updateAcmeSettings,
updateCertificateRenewSettings,
} from "../services/certificates-service.js"
const certificatesRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/certificates", async (_req, reply) => {
return reply.send(await listCertificatesFromServers())
})
app.post("/certificates/refresh", async (_req, reply) => {
return reply.send(await listCertificatesFromServers())
})
app.get("/certificates/acme-settings", async (_req, reply) => {
return reply.send(await getAcmeSettingsPublic())
})
app.put("/certificates/acme-settings", async (req, reply) => {
const parsed = putAcmeCloudflareSettingsSchema.safeParse(req.body ?? {})
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
return reply.send(await updateAcmeSettings(parsed.data))
})
app.get("/certificates/renew-settings", async (_req, reply) => {
return reply.send(await getCertificateRenewSettings())
})
app.put("/certificates/renew-settings", async (req, reply) => {
const parsed = putCertificateRenewSettingsSchema.safeParse(req.body ?? {})
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const result = await updateCertificateRenewSettings(parsed.data)
await refreshScheduler()
return reply.send(result)
})
app.post("/certificates/acme-settings/test", async (req, reply) => {
const parsed = testAcmeCloudflareSettingsSchema.safeParse(req.body ?? {})
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const effective = parsed.data.cloudflareApiToken?.trim() || await getAcmeCloudflareToken()
if (!effective) {
return reply.status(400).send({ error: "Не задан Cloudflare API token" })
}
try {
await testCloudflareToken(effective)
return reply.send({ ok: true, message: "Cloudflare API доступен" })
} catch (e) {
return reply.status(400).send({
ok: false,
message: e instanceof Error ? e.message : String(e),
})
}
})
app.post("/certificates/issue", async (req, reply) => {
const parsed = certificateIssueRequestSchema.safeParse(req.body ?? {})
if (!parsed.success) {
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
}
const serverId = String(parsed.data.serverId)
const server = await getServerRowByIdString(serverId)
if (!server) return reply.status(404).send({ error: "Сервер не найден" })
const jobId = randomUUID()
const trustStore = (parsed.data.trustStore ?? ["www", "api"]).join(",")
await createIssueJobRecord({
id: jobId,
serverId,
certName: parsed.data.certName.trim(),
domainNames: parsed.data.domainNames.map((d) => d.trim()).filter(Boolean),
keyType: parsed.data.keyType ?? "rsa2048",
trustStore,
source: "manual",
})
queueCertificateIssueJob(jobId)
return reply.send({ jobId })
})
app.get("/certificates/issue/:jobId", async (req, reply) => {
const jobId = String((req.params as { jobId: string }).jobId)
const row = await getIssueJobRecord(jobId)
if (!row) return reply.status(404).send({ error: "Задача не найдена" })
return reply.send(await toIssueJobDto(row))
})
}
export default certificatesRoutes