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
При старте backend накатывает схему PostgreSQL 18 и, если база пустая, один раз импортирует mikrotik.db с тома. Повторный старт не копирует данные. Бэкап в UI идёт через pg_dump. Co-authored-by: Cursor <[email protected]>
113 lines
4.3 KiB
TypeScript
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
|