fix(alerts): защитить тест правила общим замком Telegram
This commit is contained in:
1 parent
9c1d14635a
commit
dc5594ca7c
9 files changed
+110
-61
No files matched your search
+24
-47
@@ -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)}}>
|
||||
|
||||
Reference in new issue
Block a user