fix(alerts): проверять доступ и использовать единый источник тестового сообщения
This commit is contained in:
1 parent
98319c9d50
commit
3556a378f2
8 files changed
+82
-37
No files matched your search
@@ -0,0 +1,16 @@
|
||||
import assert from "node:assert/strict"
|
||||
import Fastify from "fastify"
|
||||
import {SignJWT} from "jose"
|
||||
process.env.AUTH_REQUIRED="true";process.env.AUTH_JWT_SECRET="evolution-fixture-secret";process.env.AUTH_ISSUER="https://auth.test.local";process.env.NODE_ENV="test"
|
||||
const {default:authPlugin,requireAuth}=await import("./auth.js")
|
||||
const {createAlertsRoutes,defaultAlertsDependencies}=await import("../routes/alerts.js")
|
||||
const app=Fastify();await app.register(authPlugin);app.addHook("preHandler",requireAuth)
|
||||
let writes=0,sends=0,reads=0
|
||||
await app.register(createAlertsRoutes({...defaultAlertsDependencies,telegram:async()=>({chatId:"123",tokenConfigured:true,messageThreadId:null}),groups:async()=>[],rules:async()=>[],history:async()=>[],meta:async()=>({servers:[],greClients:[],probeTargets:[],rttLossTargets:[],trafficServers:[],serversDetail:[]}),writeTelegram:async()=>{writes++;return {chatId:"123",tokenConfigured:true,messageThreadId:null}},readTelegram:async()=>{reads++;return {botToken:"fixture-only",chatId:"123",messageThreadId:null}},send:async()=>{sends++;return {ok:true}},replace:async()=>{writes++}}),{prefix:"/api"})
|
||||
for(const permission of ["mm:alerts:read","mm:alerts:write","mm:alerts:admin","mm:settings:admin"]){
|
||||
const token=await new SignJWT({sub:"fixture",apps:["mm"],permissions:[permission]}).setProtectedHeader({alg:"HS256"}).setIssuer("https://auth.test.local").setExpirationTime("1h").sign(new TextEncoder().encode("evolution-fixture-secret"))
|
||||
for(const [method,url] of [["GET","/api/alerts"],["PUT","/api/alerts/telegram"],["POST","/api/alerts/telegram/test"],["PUT","/api/alerts/rules"]] as const){const permitted=permission==="mm:alerts:write"||permission==="mm:alerts:admin"||method==="GET"&&permission==="mm:alerts:read";const response=await app.inject({method,url,headers:{authorization:"Bearer "+token},...(method==="PUT"?{payload:url.endsWith("rules")?{rules:[],groups:[]}:{chatId:"123"}}:{})});assert.equal(response.statusCode,permitted?200:403);assert.equal(response.body.includes("fixture-only"),false)}
|
||||
}
|
||||
assert.equal(writes,4);assert.equal(sends,2);assert.equal(reads,2)
|
||||
for(const [method,url] of [["GET","/api/alerts"],["POST","/api/alerts/telegram/test"]] as const)assert.equal((await app.inject({method,url})).statusCode,401)
|
||||
assert.equal(sends,2);await app.close();console.log("Alerts JWT: real read/write/admin gates before save/send/read, no token echo")
|
||||
@@ -0,0 +1,16 @@
|
||||
import assert from "node:assert/strict"
|
||||
import Fastify from "fastify"
|
||||
import {createAlertsRoutes,defaultAlertsDependencies} from "./alerts.js"
|
||||
import {executeTelegramSettingsUpdate} from "../services/alerts-telegram-settings.js"
|
||||
let stored={botToken:"fixture-secret",chatId:"123",messageThreadId:10 as number|null},reads=0,writes=0,sends=0,failSend=false,failWrite=false
|
||||
const app=Fastify();await app.register(createAlertsRoutes({...defaultAlertsDependencies,meta:async()=>({servers:[],greClients:[],probeTargets:[],rttLossTargets:[],trafficServers:[],serversDetail:[]}),telegram:async()=>({chatId:stored.chatId,tokenConfigured:!!stored.botToken,messageThreadId:stored.messageThreadId}),groups:async()=>[],rules:async()=>[],history:async()=>[],replace:async()=>{},writeTelegram:async patch=>executeTelegramSettingsUpdate(patch,{ensure:async()=>{},update:async values=>{writes++;if(failWrite)return undefined;stored={...stored,...values};return {...stored}}}),readTelegram:async()=>{reads++;return {...stored}},send:async input=>{sends++;assert.equal(input.token,"fixture-secret");assert.equal(input.chatId,"123");assert.equal(input.messageThreadId,10);return failSend?{ok:false,error:"send unknown"}:{ok:true}}}),{prefix:"/api"})
|
||||
const test=()=>app.inject({method:"POST",url:"/api/alerts/telegram/test",payload:{}})
|
||||
let response=await app.inject({method:"GET",url:"/api/alerts"});assert.equal(response.statusCode,200);assert.equal(response.body.includes("fixture-secret"),false);assert.deepEqual(response.json().rules,[])
|
||||
response=await test();assert.equal(response.statusCode,200);assert.deepEqual(response.json(),{ok:true});assert.equal(reads,1);assert.equal(sends,1)
|
||||
failSend=true;response=await test();assert.equal(response.statusCode,502);assert.equal(response.json().ok,false);assert.equal(reads,2);assert.equal(sends,2)
|
||||
for(const payload of [{messageThreadId:0},{messageThreadId:1.2},{messageThreadId:true},{messageThreadId:"1"},{messageThreadId:2147483648},{unknown:true}]){response=await app.inject({method:"POST",url:"/api/alerts/telegram/test",payload});assert.equal(response.statusCode,400)}assert.equal(sends,2);assert.equal(reads,2)
|
||||
for(const payload of [{},{token:4},{messageThreadId:1.5},{messageThreadId:2147483648},{unknown:true}]){response=await app.inject({method:"PUT",url:"/api/alerts/telegram",payload});assert.equal(response.statusCode,400)}assert.equal(writes,0)
|
||||
response=await app.inject({method:"PUT",url:"/api/alerts/telegram",payload:{chatId:" new "}});assert.equal(response.statusCode,200);assert.equal(response.json().telegram.chatId,"new");assert.equal(stored.botToken,"fixture-secret");assert.equal(stored.messageThreadId,10);assert.equal(response.body.includes("fixture-secret"),false);assert.equal(writes,1)
|
||||
failWrite=true;response=await app.inject({method:"PUT",url:"/api/alerts/telegram",payload:{chatId:"other"}});assert.equal(response.statusCode,500);assert.equal(response.json().ok,undefined);assert.equal(writes,2)
|
||||
stored={...stored,botToken:""};response=await test();assert.equal(response.statusCode,400);assert.equal(sends,2)
|
||||
await app.close();console.log("Alerts HTTP: one stored test snapshot, strict numeric thread, selective save, unknown send/no retry, missing returning/no success, no token echo")
|
||||
@@ -1,33 +1,38 @@
|
||||
import {sendTelegramRequest} from "../services/alerts-telegram-send.js"
|
||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||
import {
|
||||
getAlertsMeta,
|
||||
getTelegramBotToken,
|
||||
getTelegramMessageThreadIdForApi,
|
||||
getTelegramPublic,
|
||||
getTelegramSettingsRow,
|
||||
listAlertGroups,
|
||||
listAlertRules,
|
||||
listMergedHistory,
|
||||
replaceAlertsConfig,
|
||||
formatAlertRuleTestTelegramText,
|
||||
sendTelegramAlertMessage,
|
||||
updateTelegramSettings,
|
||||
} from "../services/alerts-service.js"
|
||||
import {
|
||||
putRulesSchema,
|
||||
putTelegramSchema,
|
||||
testTelegramSchema,
|
||||
telegramSettingsReceiptSchema,
|
||||
} from "@mmapp/contracts/alerts"
|
||||
|
||||
const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
interface Dependencies {
|
||||
meta:typeof getAlertsMeta;telegram:typeof getTelegramPublic;groups:typeof listAlertGroups;rules:typeof listAlertRules;history:typeof listMergedHistory;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 tg = await getTelegramPublic()
|
||||
const tg = await deps.telegram()
|
||||
const connected = tg.tokenConfigured && Boolean(tg.chatId?.trim())
|
||||
return reply.send({
|
||||
telegram: { ...tg, connected },
|
||||
groups: await listAlertGroups(),
|
||||
rules: await listAlertRules(),
|
||||
history: await listMergedHistory(),
|
||||
meta: await getAlertsMeta(),
|
||||
groups: await deps.groups(),
|
||||
rules: await deps.rules(),
|
||||
history: await deps.history(),
|
||||
meta: await deps.meta(),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -38,7 +43,7 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
}
|
||||
const groups =
|
||||
parsed.data.groups ??
|
||||
(await listAlertGroups()).map((g) => ({
|
||||
(await deps.groups()).map((g) => ({
|
||||
id: g.id,
|
||||
name: g.name,
|
||||
combineMode: g.combineMode,
|
||||
@@ -67,8 +72,8 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
recoveryStabilitySec: r.recoveryStabilitySec ?? null,
|
||||
chatId: r.chatId,
|
||||
}))
|
||||
await replaceAlertsConfig({ groups, rules })
|
||||
return reply.send({ ok: true, groups: await listAlertGroups(), rules: await listAlertRules() })
|
||||
await deps.replace({ groups, rules })
|
||||
return reply.send({ ok: true, groups: await deps.groups(), rules: await deps.rules() })
|
||||
})
|
||||
|
||||
app.put("/alerts/telegram", async (req, reply) => {
|
||||
@@ -77,48 +82,45 @@ const alertsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
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 updateTelegramSettings({
|
||||
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())
|
||||
return reply.send({ ok: true, telegram: { ...next, connected } })
|
||||
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.safeParse(req.body ?? {})
|
||||
const parsed = testTelegramSchema.strict().safeParse(req.body ?? {})
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() })
|
||||
}
|
||||
const fromDb = await getTelegramBotToken()
|
||||
const token = (parsed.data.token?.trim() || fromDb).trim()
|
||||
const pub = await getTelegramPublic()
|
||||
const chatId = (parsed.data.chatId?.trim() || pub.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 != null && parsed.data.messageThreadId >= 1
|
||||
? parsed.data.messageThreadId
|
||||
: await getTelegramMessageThreadIdForApi()
|
||||
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 sendTelegramAlertMessage({
|
||||
const send = await deps.send({
|
||||
text,
|
||||
token: parsed.data.token?.trim() || undefined,
|
||||
token,
|
||||
chatId,
|
||||
messageThreadId: threadId,
|
||||
})
|
||||
if (!send.ok) {
|
||||
if (send.ok !== true) {
|
||||
return reply.status(502).send({ ok: false, error: send.error })
|
||||
}
|
||||
return reply.send({ ok: true })
|
||||
})
|
||||
}
|
||||
|
||||
export default alertsRoutes
|
||||
}
|
||||
export const defaultAlertsDependencies:Dependencies={meta:getAlertsMeta,telegram:getTelegramPublic,groups:listAlertGroups,rules:listAlertRules,history:listMergedHistory,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)
|
||||
Reference in new issue
Block a user