fix(alerts): защитить тест правила общим замком Telegram

This commit is contained in:
Denozordec committed 2026-10-03 18:20:29 +07:00
1 parent 9c1d14635a
commit dc5594ca7c
9 files changed
+110 -61

No files matched your search

+24 -47
View File
@@ -2,6 +2,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import {can} from "@/lib/auth"
import {useAlertsTelegramPreview} from "@/hooks/use-alerts-telegram-preview"
import {useAlertsConfigWrite} from "@/hooks/use-alerts-config-write"
import {AlertsGroupForm} from "@/components/alerts-group-form"
import {useAlertsTargets} from "@/hooks/use-alerts-targets"
@@ -46,8 +47,6 @@ import {
import { Flag, countryName } from "@/components/flag"
import { cn } from "@/lib/utils"
import {getAlerts} from "@/shared/api/alerts"
import { requestJson } from "@/shared/api/http-client"
import { toast } from "sonner"
// ─── types ────────────────────────────────────────────────────────────────────
@@ -177,11 +176,7 @@ interface AlertsMeta {
greTunnelDetail?: AlertsGreTunnelRow[]
}
function makeApiFetch(backendUrl: string) {
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
return requestJson<T>(backendUrl, path, init)
}
}
function formatRelativeRu(iso: string): string {
const t = Date.parse(iso)
@@ -1539,7 +1534,7 @@ function AddRuleSheet({
initialForm,
editRuleId,
onTestRuleTelegram,
testTelegramDisabled, saving,saveDisabled,saveError,review,
testTelegramDisabled, saving,saveDisabled,saveError,review,testOutcome,testError,testReview,
}: {
open: boolean
onClose: () => void
@@ -1549,6 +1544,9 @@ function AddRuleSheet({
saveDisabled:boolean
saveError:string|null
review:React.ReactNode
testOutcome:string|null
testError:string|null
testReview:React.ReactNode
typeTargets: Record<AlertType, string[]>
isLive: boolean
serversCatalog: AlertsServerRow[]
@@ -1557,7 +1555,7 @@ function AddRuleSheet({
initialForm: RuleFormSnapshot | null
editRuleId: string | null
/** Live: отправка тестового сообщения с текстом как у сохранённого правила (без записи в БД). */
onTestRuleTelegram?: (payload: RuleTelegramTestPayload) => Promise<void>
onTestRuleTelegram?: (payload: RuleTelegramTestPayload) => Promise<boolean>
/** true — кнопка теста неактивна (например, бэкенд недоступен). */
testTelegramDisabled?: boolean
}) {
@@ -1682,9 +1680,6 @@ function AddRuleSheet({
cooldown: form.cooldown,
ruleChatId: form.chatId.trim(),
})
toast.success("Тестовое сообщение отправлено в Telegram.")
} catch (e) {
toast.error(e instanceof Error ? e.message : "Не удалось отправить тест")
} finally {
setTestTelegramBusy(false)
}
@@ -2194,7 +2189,7 @@ function AddRuleSheet({
disabled={!canSave || testTelegramBusy || testTelegramDisabled||saving}
title={
testTelegramDisabled
? "Тест из формы будет доступен после подключения защиты повторной отправки"
? "Проверьте права, доступность, сохранённый токен и результат предыдущего запроса"
: !canSave
? "Заполните название, объект, условие и при необходимости порог"
: undefined
@@ -2209,6 +2204,9 @@ function AddRuleSheet({
Тест в Telegram
</Button>
) : null}
{testError?<p role="alert" className="w-full text-xs text-destructive break-words">{testError}</p>:null}
{testOutcome?<p role="status" className="w-full text-xs break-words">{testOutcome}</p>:null}
{testReview}
{review}
{saveError?<p role="alert" className="w-full text-xs text-destructive break-words">{saveError}</p>:null}
<Button type="button" className="w-full sm:flex-1 min-h-9" onClick={()=>void handleSave()} disabled={!canSave||saveDisabled||saving}>
@@ -2229,7 +2227,6 @@ export default function AlertsPage(){const {mode,backendUrl}=useDataSource();ret
function AlertsContent() {
const { mode, backendUrl, backendStatus } = useDataSource()
const isLive = mode === "live"
const apiFetch = useMemo(() => makeApiFetch(backendUrl), [backendUrl])
const [rules, setRules] = useState<AlertRule[]>(isLive?[]:INIT_RULES)
const [groups, setGroups] = useState<AlertGroup[]>(isLive?[]:INIT_GROUPS)
@@ -2271,6 +2268,7 @@ function AlertsContent() {
const beginOperation=()=>{if(!pageActive.current||operationLock.current)return false;operationLock.current=true;reader.current?.abort();revision.current++;setLoadBusy(false);setOperationBusy(true);return true}
const endOperation=()=>{operationLock.current=false;setOperationBusy(false)}
const config=useAlertsConfigWrite(backendUrl,isLive,known&&backendStatus===true,beginOperation,endOperation)
const preview=useAlertsTelegramPreview(backendUrl,isLive,known&&backendStatus===true&&tokenConfigured,beginOperation,endOperation)
const rulesSaveBusy=config.busy
const writesDisabled=!config.enabled||operationBusy||config.unknown
const [pendingDelete,setPendingDelete]=useState<{kind:"rule"|"group";id:string}|null>(null)
@@ -2358,34 +2356,11 @@ function AlertsContent() {
setRules(mapApiRules(receipt.rules));setGroups(mapApiGroups(receipt.groups));return true
},[config])
const testRuleTelegramFromForm = useCallback(
async (p: RuleTelegramTestPayload) => {
if (!isLive) return
const threadRaw = tg.messageThreadId.trim()
const messageThreadId =
threadRaw && Number.isFinite(Number.parseInt(threadRaw, 10))
? Number.parseInt(threadRaw, 10)
: undefined
await apiFetch("/api/alerts/telegram/test", {
method: "POST",
body: JSON.stringify({
token: tg.token.trim() || undefined,
chatId: p.ruleChatId.trim() || tg.chatId.trim() || undefined,
...(messageThreadId != null && Number.isFinite(messageThreadId) && messageThreadId >= 1
? { messageThreadId }
: {}),
rulePreview: {
name: p.name,
targets: p.targets,
conditionLine: p.conditionLine,
severity: p.severity,
cooldown: p.cooldown,
},
}),
})
},
[apiFetch, isLive, tg.token, tg.chatId, tg.messageThreadId],
)
const testRuleTelegramFromForm=async(p:RuleTelegramTestPayload)=>preview.test({
chatId:p.ruleChatId.trim()||tg.chatId.trim()||undefined,
...(tg.messageThreadId?{messageThreadId:Number(tg.messageThreadId)}:{}),
rulePreview:{name:p.name,targets:p.targets,conditionLine:p.conditionLine,severity:p.severity,cooldown:p.cooldown},
})
const serverNamesForPresets = useMemo(() => {
if (isLive) return meta?.servers??[]
@@ -2498,21 +2473,21 @@ function AlertsContent() {
const openRuleSheetAdd = useCallback(() => {
if(!config.enabled||operationLock.current||config.unknown)return
setSheetEditId(null)
preview.clear();setSheetEditId(null)
setSheetSnap(null)
setSheetKey((k) => k + 1)
setSheetOpen(true)
}, [config.enabled,config.unknown])
}, [config.enabled,config.unknown,preview])
const openRuleSheetEdit = useCallback((id: string) => {
if(!config.enabled||operationLock.current||config.unknown)return
const r = rules.find((x) => x.id === id)
if (!r) return
setSheetSnap(ruleToFormSnapshot(r))
preview.clear();setSheetSnap(ruleToFormSnapshot(r))
setSheetEditId(id)
setSheetKey((k) => k + 1)
setSheetOpen(true)
}, [rules,config.enabled,config.unknown])
}, [rules,config.enabled,config.unknown,preview])
const handleRuleSheetSave = useCallback(
async (payload: Omit<AlertRule, "id" | "lastFired" | "enabled">, editId: string | null) => {
@@ -2849,7 +2824,9 @@ function AlertsContent() {
initialForm={sheetSnap}
editRuleId={sheetEditId}
onTestRuleTelegram={isLive ? testRuleTelegramFromForm : undefined}
testTelegramDisabled={true}
testTelegramDisabled={!preview.enabled||operationBusy||preview.unknown}
testOutcome={preview.outcome} testError={preview.error}
testReview={preview.unknown&&!preview.busy?<div className="w-full space-y-2"><p role="status" className="text-xs">Результат Telegram неизвестен. Проверьте чат перед повтором: сообщение могло отправиться. Черновик правила не записан.</p><Button type="button" size="sm" variant="outline" className="h-auto whitespace-normal" disabled={!preview.enabled||operationBusy} onClick={preview.review}>Я проверил Telegram — разрешить новый запрос</Button></div>:null}
/>
<AlertDialog open={pendingDelete!==null} onOpenChange={open=>{if(!open&&!operationLock.current)setPendingDelete(null)}}>