fix(alerts): защитить тест правила общим замком Telegram
This commit is contained in:
1 parent
9c1d14635a
commit
dc5594ca7c
9 files changed
+110
-61
No files matched your search
@@ -1,8 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import {useEffect,useRef,useState} from "react"
|
||||
import {useEffect,useRef,useState,useSyncExternalStore} from "react"
|
||||
import {EyeIcon,EyeOffIcon} from "lucide-react"
|
||||
import type {PutTelegramBody} from "@mmapp/contracts/alerts"
|
||||
import {telegramPending,subscribeTelegramPending,setTelegramPending as checkpoint} from "@/lib/alerts-telegram-checkpoint"
|
||||
import {can} from "@/lib/auth"
|
||||
import {ApiClientError} from "@/shared/api/http-client"
|
||||
import {saveAlertsTelegram,testAlertsTelegram} from "@/shared/api/alerts"
|
||||
@@ -11,24 +12,21 @@ import {Button} from "@/components/ui/button"
|
||||
import {Input} from "@/components/ui/input"
|
||||
|
||||
export type TelegramDraft={token:string;chatId:string;messageThreadId:string;connected:boolean}
|
||||
const pending=new Set<string>()
|
||||
const storageKey=(source:string)=>"mm:alerts-telegram-pending:"+source
|
||||
function unresolved(source:string){try{return pending.has(source)||sessionStorage.getItem(storageKey(source))==="true"}catch{return pending.has(source)}}
|
||||
function checkpoint(source:string,value:boolean){if(value)pending.add(source);else pending.delete(source);try{if(value)sessionStorage.setItem(storageKey(source),"true");else sessionStorage.removeItem(storageKey(source))}catch{/* The memory guard remains active during this browser lifetime. */}}
|
||||
function thread(value:string){if(!value.trim())return null;if(!/^\d+$/.test(value.trim()))throw Error("ID темы: укажите положительное целое число или оставьте поле пустым");const n=Number(value);if(!Number.isSafeInteger(n)||n<1||n>2147483647)throw Error("ID темы должен быть от 1 до 2147483647");return n}
|
||||
|
||||
export function AlertsTelegramForm({initial,tokenConfigured,base,live,available,onBegin,onEnd,onSaved}:{initial:TelegramDraft;tokenConfigured:boolean;base:string;live:boolean;available:boolean;onBegin:()=>boolean;onEnd:()=>void;onSaved:(value:TelegramDraft,configured:boolean)=>void}){
|
||||
const source=JSON.stringify([live,base])
|
||||
const [draft,setDraft]=useState<Partial<Pick<TelegramDraft,"token"|"chatId"|"messageThreadId">>>({})
|
||||
const [showToken,setShowToken]=useState(false),[busy,setBusy]=useState<"save"|"test"|null>(null),[error,setError]=useState<string|null>(null),[outcome,setOutcome]=useState<string|null>(null),[unknown,setUnknown]=useState(false),[ready,setReady]=useState(false)
|
||||
const [showToken,setShowToken]=useState(false),[busy,setBusy]=useState<"save"|"test"|null>(null),[error,setError]=useState<string|null>(null),[outcome,setOutcome]=useState<string|null>(null),[ready,setReady]=useState(false)
|
||||
const unknown=useSyncExternalStore(subscribeTelegramPending,()=>telegramPending(source),()=>false)
|
||||
const active=useRef(true),lock=useRef(false),controller=useRef<AbortController|null>(null)
|
||||
useEffect(()=>{active.current=true;queueMicrotask(()=>{if(active.current){setUnknown(unresolved(source));setReady(true)}});return()=>{active.current=false;controller.current?.abort()}},[source])
|
||||
useEffect(()=>{active.current=true;queueMicrotask(()=>{if(active.current){setReady(true)}});return()=>{active.current=false;controller.current?.abort()}},[source])
|
||||
const value={...initial,...draft},allowed=can("mm:alerts:write"),enabled=ready&&live&&available&&allowed
|
||||
const dirty=Object.keys(draft).some(key=>value[key as keyof typeof draft]!==initial[key as keyof typeof draft])
|
||||
function current(){return active.current&&can("mm:alerts:write")}
|
||||
function change(key:keyof typeof draft,value:string){setDraft(previous=>({...previous,[key]:value}));setError(null);setOutcome(null)}
|
||||
async function execute(kind:"save"|"test"){
|
||||
if(!enabled||lock.current||unknown||!current())return
|
||||
if(!enabled||lock.current||unknown||telegramPending(source)||!current())return
|
||||
const payload:PutTelegramBody={}
|
||||
try{
|
||||
const messageThreadId=thread(value.messageThreadId)
|
||||
@@ -45,7 +43,7 @@ export function AlertsTelegramForm({initial,tokenConfigured,base,live,available,
|
||||
}catch(e){setError(e instanceof Error?e.message:"Проверьте параметры Telegram");return}
|
||||
if(!onBegin())return
|
||||
lock.current=true;controller.current=new AbortController();setBusy(kind);setError(null);setOutcome(null)
|
||||
checkpoint(source,true);setUnknown(true)
|
||||
checkpoint(source,true)
|
||||
try{
|
||||
if(kind==="save"){
|
||||
const result=await saveAlertsTelegram(base,payload,{signal:controller.current.signal});if(!current())return
|
||||
@@ -57,11 +55,11 @@ export function AlertsTelegramForm({initial,tokenConfigured,base,live,available,
|
||||
await testAlertsTelegram(base,{chatId:value.chatId.trim(),...(value.token.trim()?{token:value.token.trim()}:{}),...(messageThreadId!==null?{messageThreadId}:{})},{signal:controller.current.signal});if(!current())return
|
||||
setOutcome("Telegram API подтвердил приём тестового сообщения. Проверьте его в чате.")
|
||||
}
|
||||
checkpoint(source,false);setUnknown(false)
|
||||
checkpoint(source,false)
|
||||
}catch(e){
|
||||
const rejected=e instanceof ApiClientError&&[400,401,403,409,422].includes(e.status)
|
||||
if(rejected&¤t())checkpoint(source,false)
|
||||
if(current()){setUnknown(!rejected);setError(e instanceof Error?e.message:"Результат Telegram не получен")}
|
||||
if(current()){setError(e instanceof Error?e.message:"Результат Telegram не получен")}
|
||||
}finally{lock.current=false;controller.current=null;if(active.current){setBusy(null);onEnd()}}
|
||||
}
|
||||
return <section aria-label="Параметры Telegram" className="min-w-0"><OpsPanel title="Telegram" description={initial.connected?"Параметры заполнены. Доставка проверяется отдельно.":"Параметры не заполнены."} contentClassName="flex min-w-0 flex-col gap-3 px-5 py-4">
|
||||
@@ -73,7 +71,7 @@ export function AlertsTelegramForm({initial,tokenConfigured,base,live,available,
|
||||
{error?<p role="alert" className="text-xs text-destructive break-words">{error}</p>:null}
|
||||
{outcome?<p role="status" className="text-xs break-words">{outcome}</p>:null}
|
||||
{busy?<p role="status" className="text-xs text-muted-foreground">Ожидаем подтверждение сервера…</p>:null}
|
||||
{unknown&&!busy?<div className="space-y-2"><p role="status" className="text-xs break-words">Результат запроса неизвестен. Он мог выполниться. Перед повтором проверьте параметры в БД или сообщение в Telegram; обновление данных не подтверждает отправку.</p><Button type="button" size="sm" variant="outline" className="h-auto whitespace-normal" disabled={!!busy||!enabled} onClick={()=>{checkpoint(source,false);setUnknown(false)}}>Я проверил результат — разрешить новый запрос</Button></div>:null}
|
||||
{unknown&&!busy?<div className="space-y-2"><p role="status" className="text-xs break-words">Результат запроса неизвестен. Он мог выполниться. Перед повтором проверьте параметры в БД или сообщение в Telegram; обновление данных не подтверждает отправку.</p><Button type="button" size="sm" variant="outline" className="h-auto whitespace-normal" disabled={!!busy||!enabled} onClick={()=>{checkpoint(source,false)}}>Я проверил результат — разрешить новый запрос</Button></div>:null}
|
||||
<Button type="button" size="sm" disabled={!enabled||!!busy||unknown||!dirty} onClick={()=>void execute("save")}>{busy==="save"?"Сохранение…":"Сохранить в БД"}</Button>
|
||||
<Button type="button" size="sm" variant="outline" disabled={!enabled||!!busy||unknown} onClick={()=>void execute("test")}>{busy==="test"?"Отправка…":"Отправить тестовое сообщение"}</Button>
|
||||
<Button type="button" size="sm" variant="ghost" disabled={!dirty||!!busy} onClick={()=>{setDraft({});setError(null);setOutcome(null)}}>Сбросить черновик</Button>
|
||||
|
||||
Reference in new issue
Block a user