131 lines
7.5 KiB
TypeScript
131 lines
7.5 KiB
TypeScript
import {getAlertsOutboxOverview,retryUnknownOutbox} from "../services/alert-engine/outbox.js"
|
|
import {sendTelegramRequest} from "../services/alerts-telegram-send.js"
|
|
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
|
import {
|
|
getAlertsOverview,
|
|
getTelegramSettingsRow,
|
|
replaceAlertsConfig,
|
|
formatAlertRuleTestTelegramText,
|
|
updateTelegramSettings,
|
|
} from "../services/alerts-service.js"
|
|
import {
|
|
alertsResponseSchema, alertsOutboxOverviewSchema,alertsOutboxRetrySchema,alertsOutboxRetryReceiptSchema,
|
|
putRulesSchema,
|
|
normalizeAlertsConfig,
|
|
putTelegramSchema,
|
|
testTelegramSchema,
|
|
telegramSettingsReceiptSchema,
|
|
} from "@mmapp/contracts/alerts"
|
|
|
|
import {OutboxRetryConflict} from "../services/alerts-outbox-retry.js"
|
|
interface Dependencies {
|
|
retryOutbox:typeof retryUnknownOutbox;
|
|
outbox:()=>Promise<unknown>;overview:()=>Promise<unknown>;replace:typeof replaceAlertsConfig;writeTelegram:typeof updateTelegramSettings
|
|
readTelegram:()=>Promise<{botToken:string;chatId:string;messageThreadId:number|null}>
|
|
send:(input:{token:string;chatId:string;text:string;messageThreadId?:number})=>Promise<{ok:true}|{ok:false;error:string}>
|
|
}
|
|
export function createAlertsRoutes(deps:Dependencies):FastifyPluginAsyncZod {return async(app)=>{
|
|
app.get("/alerts", async (_req, reply) => {
|
|
const result=alertsResponseSchema.safeParse(await deps.overview())
|
|
if(!result.success)return reply.status(500).send({error:"Снимок оповещений не подтверждён"})
|
|
return reply.header("Cache-Control","no-store").send(result.data)
|
|
})
|
|
|
|
app.get("/alerts/outbox",async(_req,reply)=>{
|
|
const parsed=alertsOutboxOverviewSchema.safeParse(await deps.outbox())
|
|
if(!parsed.success)return reply.status(500).send({error:"Состояние очереди не подтверждено"})
|
|
return reply.header("Cache-Control","no-store").send(parsed.data)
|
|
})
|
|
|
|
app.post<{Params:{id:string}}>("/alerts/outbox/:id/retry",async(req,reply)=>{
|
|
const parsed=alertsOutboxRetrySchema.safeParse(req.body)
|
|
if(!parsed.success||!req.params.id.trim()||req.params.id.length>200)return reply.status(400).send({error:"Проверьте подтверждение и версию записи очереди"})
|
|
try{const receipt=alertsOutboxRetryReceiptSchema.safeParse(await deps.retryOutbox(req.params.id,parsed.data));if(!receipt.success||receipt.data.id!==req.params.id||receipt.data.resolutionVersion!==parsed.data.expectedVersion+1)return reply.status(500).send({error:"Постановка сообщения в очередь не подтверждена"});return reply.header("Cache-Control","no-store").send(receipt.data)}catch(error){if(error instanceof OutboxRetryConflict)return reply.status(409).send({error:error.message});throw error}
|
|
})
|
|
|
|
app.put("/alerts/rules", async (req, reply) => {
|
|
const parsed = putRulesSchema.safeParse(req.body)
|
|
if (!parsed.success) {
|
|
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
|
}
|
|
try { normalizeAlertsConfig(parsed.data) } catch { return reply.status(400).send({error:"Проверьте объекты, условия и группы правил"}) }
|
|
const ifMatch=req.headers["if-match"]
|
|
if(ifMatch===undefined)return reply.status(428).send({error:"Обновите снимок правил перед сохранением"})
|
|
if(typeof ifMatch!=="string"||!/^"[a-f0-9]{64}"$/.test(ifMatch))return reply.status(400).send({error:"Некорректная ревизия правил"})
|
|
const expectedRevision=ifMatch.slice(1,-1)
|
|
const groups = parsed.data.groups
|
|
const rules = parsed.data.rules.map((r) => ({
|
|
id: r.id,
|
|
name: r.name,
|
|
type: r.type,
|
|
target: r.target,
|
|
targets: r.targets,
|
|
groupId: r.groupId ?? null,
|
|
condition: r.condition,
|
|
conditions:
|
|
r.conditions && r.conditions.length > 0
|
|
? r.conditions.map((c) => c.trim()).filter(Boolean)
|
|
: r.condition?.trim()
|
|
? [r.condition.trim()]
|
|
: [],
|
|
severity: r.severity,
|
|
enabled: r.enabled,
|
|
cooldown: r.cooldown,
|
|
confirmStabilitySec: r.confirmStabilitySec ?? null,
|
|
recoveryMode: r.recoveryMode ?? "always",
|
|
recoveryStabilitySec: r.recoveryStabilitySec ?? null,
|
|
chatId: r.chatId,
|
|
}))
|
|
try {return reply.send(await deps.replace({ groups, rules,expectedRevision }))}
|
|
catch(error){if(error instanceof Error&&error.message.includes("Правила изменились"))return reply.status(412).send({error:error.message});if(error instanceof Error&&error.message.includes("уже выполняется"))return reply.status(409).send({error:error.message});throw error}
|
|
})
|
|
|
|
app.put("/alerts/telegram", async (req, reply) => {
|
|
const parsed = putTelegramSchema.strict().safeParse(req.body)
|
|
if (!parsed.success) {
|
|
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
|
}
|
|
if (Object.keys(parsed.data).length === 0) return reply.status(400).send({error:"Нет изменённых параметров Telegram"})
|
|
const next = await deps.writeTelegram({
|
|
token: parsed.data.token,
|
|
chatId: parsed.data.chatId,
|
|
messageThreadId: parsed.data.messageThreadId,
|
|
})
|
|
const connected = next.tokenConfigured && Boolean(next.chatId?.trim())
|
|
const receipt = telegramSettingsReceiptSchema.safeParse({ ok: true, telegram: { ...next, connected } })
|
|
if (!receipt.success) return reply.status(500).send({error:"Сохранение параметров Telegram не подтверждено"})
|
|
return reply.send(receipt.data)
|
|
})
|
|
|
|
app.post("/alerts/telegram/test", async (req, reply) => {
|
|
const parsed = testTelegramSchema.strict().safeParse(req.body ?? {})
|
|
if (!parsed.success) {
|
|
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
|
}
|
|
const stored = await deps.readTelegram()
|
|
const token = (parsed.data.token?.trim() || stored.botToken).trim()
|
|
const chatId = (parsed.data.chatId?.trim() || stored.chatId || "").trim()
|
|
if (!token) return reply.status(400).send({error:"Не задан Bot Token (сохраните в БД или передайте в запросе)"})
|
|
if (!chatId) return reply.status(400).send({error:"Не задан Chat ID"})
|
|
const threadId = parsed.data.messageThreadId ?? stored.messageThreadId ?? undefined
|
|
if (threadId !== undefined && (!Number.isInteger(threadId) || threadId < 1 || threadId > 2147483647)) return reply.status(400).send({error:"ID темы Telegram не подтверждён"})
|
|
const text = parsed.data.rulePreview
|
|
? formatAlertRuleTestTelegramText(parsed.data.rulePreview)
|
|
: "MikroTik Manager — тест оповещений (Telegram)."
|
|
const send = await deps.send({
|
|
text,
|
|
token,
|
|
chatId,
|
|
messageThreadId: threadId,
|
|
})
|
|
if (send.ok !== true) {
|
|
return reply.status(502).send({ ok: false, error: send.error })
|
|
}
|
|
return reply.send({ ok: true })
|
|
})
|
|
}
|
|
|
|
}
|
|
export const defaultAlertsDependencies:Dependencies={retryOutbox:retryUnknownOutbox,outbox:getAlertsOutboxOverview,overview:getAlertsOverview,replace:replaceAlertsConfig,writeTelegram:updateTelegramSettings,readTelegram:getTelegramSettingsRow,send:input=>sendTelegramRequest(input.token,{chat_id:input.chatId,text:input.text,...(input.messageThreadId!==undefined?{message_thread_id:input.messageThreadId}:{})})}
|
|
export default createAlertsRoutes(defaultAlertsDependencies)
|