fix(ui): учитывать права на операции с серверами
This commit is contained in:
1 parent
e9e5030047
commit
61950638bc
6 files changed
+121
-18
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
import { Suspense, useEffect, useMemo, useRef, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { can } from "@/lib/auth"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { serverCreateSchema } from "@mmapp/contracts/servers"
|
||||
import { OperationGate, pollServers, serverOperationError, type PollResult } from "@/lib/server-operations"
|
||||
@@ -256,6 +257,13 @@ function ServersPageContent() {
|
||||
const formVersion = useRef(0)
|
||||
const testVersion = useRef(0)
|
||||
const contextVersion = useRef(0)
|
||||
const canWrite = can("mm:servers:write")
|
||||
const accessDenied = "Недостаточно прав: изменение и опрос серверов требуют разрешения на запись."
|
||||
function requireWrite(report: (message: string) => void = setOperationError) {
|
||||
if (can("mm:servers:write")) return true
|
||||
report(accessDenied)
|
||||
return false
|
||||
}
|
||||
const controlsBusy = saving || rowBusy || pollAllBusy || pollingIds.size > 0
|
||||
|
||||
const set = <K extends keyof FormState>(k: K, v: FormState[K]) => {
|
||||
@@ -316,6 +324,7 @@ function ServersPageContent() {
|
||||
// ── actions ──────────────────────────────────────────────────────────────
|
||||
|
||||
function openAdd() {
|
||||
if (!requireWrite()) return
|
||||
if (gate.current.has("save")) return
|
||||
formVersion.current++; testVersion.current++
|
||||
setFormError(""); setDetailsLoading(false); setDetailsReady(true)
|
||||
@@ -326,6 +335,7 @@ function ServersPageContent() {
|
||||
}
|
||||
|
||||
async function openEdit(s: Server) {
|
||||
if (!requireWrite()) return
|
||||
if (gate.current.has("save")) return
|
||||
const version = ++formVersion.current
|
||||
testVersion.current++
|
||||
@@ -370,6 +380,7 @@ function ServersPageContent() {
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!requireWrite(setFormError)) return
|
||||
if (!detailsReady || detailsLoading || !gate.current.acquire("save")) return
|
||||
const context = contextVersion.current
|
||||
setSaving(true); setFormError("")
|
||||
@@ -462,6 +473,7 @@ function ServersPageContent() {
|
||||
}
|
||||
|
||||
async function handleToggleStatus(id: string) {
|
||||
if (!requireWrite()) return
|
||||
const server = serverList.find(s => s.id === id)
|
||||
if (!server || !gate.current.acquire("rows")) return
|
||||
const context = contextVersion.current
|
||||
@@ -478,6 +490,7 @@ function ServersPageContent() {
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!requireWrite(setDeleteError)) return
|
||||
if (!gate.current.acquire("rows")) return
|
||||
const context = contextVersion.current
|
||||
setDeleting(true); setRowBusy(true); setDeleteError("")
|
||||
@@ -500,12 +513,14 @@ function ServersPageContent() {
|
||||
}
|
||||
|
||||
async function runPoll(ids: string[]) {
|
||||
if (!requireWrite()) return
|
||||
if (!isLive || !gate.current.acquire("rows")) return
|
||||
const context = contextVersion.current
|
||||
setPollAllBusy(true); setPollingIds(new Set(ids)); setOperationError(""); setOperationMessage(""); setPollResults([])
|
||||
try {
|
||||
const results = await pollServers(ids, async id => {
|
||||
if (context !== contextVersion.current) throw new Error("Контекст изменился; опрос отменён.")
|
||||
if (!can("mm:servers:write")) throw new Error(accessDenied)
|
||||
await pollServer(backendUrl, id)
|
||||
})
|
||||
if (context !== contextVersion.current) return
|
||||
@@ -522,6 +537,7 @@ function ServersPageContent() {
|
||||
}
|
||||
|
||||
async function handleTest() {
|
||||
if (!requireWrite(setFormError)) return
|
||||
if (!detailsReady || gate.current.has("save") || gate.current.has("test")) return
|
||||
const version = ++testVersion.current
|
||||
if (!form.host || !form.username) {
|
||||
@@ -595,11 +611,11 @@ function ServersPageContent() {
|
||||
crumbs={[{ label: "Управление" }, { label: "Серверы" }]}
|
||||
actions={
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={() => selectedServerId !== null ? selectedServer && void runPoll([selectedServer.id]) : void handlePollAll()} disabled={!isLive || controlsBusy || (selectedServerId !== null ? !selectedServer : serverList.length === 0)}>
|
||||
<Button variant="outline" size="sm" onClick={() => selectedServerId !== null ? selectedServer && void runPoll([selectedServer.id]) : void handlePollAll()} disabled={!canWrite || !isLive || controlsBusy || (selectedServerId !== null ? !selectedServer : serverList.length === 0)}>
|
||||
<RefreshCwIcon className={cn("size-4", pollAllBusy && "animate-spin")} />{selectedServerId !== null ? "Проверить сервер" : "Проверить все"}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm"><DownloadIcon className="size-4" />Экспорт</Button>
|
||||
<Button size="sm" onClick={openAdd} disabled={controlsBusy}><PlusIcon className="size-4" />Добавить сервер</Button>
|
||||
<Button size="sm" onClick={openAdd} disabled={!canWrite || controlsBusy}><PlusIcon className="size-4" />Добавить сервер</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -607,6 +623,7 @@ function ServersPageContent() {
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
<div className="flex flex-col gap-5">
|
||||
|
||||
{!canWrite && <Alert variant="info" role="status"><AlertTitle>Только просмотр</AlertTitle><AlertDescription>Изменение, удаление, опрос серверов и тест соединения требуют разрешения на запись. Просмотр деталей и фильтры доступны.</AlertDescription></Alert>}
|
||||
{operationError && <Alert variant="destructive" role="alert"><AlertTitle>Действие не завершено</AlertTitle><AlertDescription>{operationError}</AlertDescription></Alert>}
|
||||
{operationMessage && <Alert variant="success" role="status"><AlertDescription>{operationMessage}</AlertDescription></Alert>}
|
||||
{pollResults.length > 0 && (
|
||||
@@ -614,7 +631,7 @@ function ServersPageContent() {
|
||||
<AlertTitle>Опрос: успешно {pollResults.filter(r => !r.error).length} из {pollResults.length}</AlertTitle>
|
||||
<AlertDescription>
|
||||
<ul>{pollResults.map(r => <li key={r.id}>{serverList.find(s => s.id === r.id)?.name ?? r.id}: {r.error ?? "опрос выполнен"}</li>)}</ul>
|
||||
{pollResults.some(r => r.error) && <Button type="button" variant="outline" size="sm" disabled={controlsBusy} onClick={() => runPoll(pollResults.filter(r => r.error).map(r => r.id))}>Повторить неуспешные</Button>}
|
||||
{pollResults.some(r => r.error) && <Button type="button" variant="outline" size="sm" disabled={!canWrite || controlsBusy} onClick={() => runPoll(pollResults.filter(r => r.error).map(r => r.id))}>Повторить неуспешные</Button>}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
@@ -692,9 +709,10 @@ function ServersPageContent() {
|
||||
isLive={isLive}
|
||||
pollingIds={pollingIds}
|
||||
busy={controlsBusy}
|
||||
canWrite={canWrite}
|
||||
onPoll={handlePoll}
|
||||
onEdit={openEdit}
|
||||
onDelete={id => { setDeleteError(""); setDeleteTarget(serverList.find(s => s.id === id) ?? null) }}
|
||||
onDelete={id => { if (!requireWrite()) return; setDeleteError(""); setDeleteTarget(serverList.find(s => s.id === id) ?? null) }}
|
||||
onToggleStatus={handleToggleStatus}
|
||||
/>
|
||||
</DataPageCard>
|
||||
@@ -714,7 +732,7 @@ function ServersPageContent() {
|
||||
{!detailsReady && !detailsLoading && editingId && <Button type="button" variant="outline" onClick={() => { const server = serverList.find(s => s.id === editingId); if (server) void openEdit(server) }}>Повторить загрузку</Button>}
|
||||
</div>}
|
||||
|
||||
<fieldset disabled={saving || !detailsReady} className="flex min-h-0 flex-1 flex-col" aria-busy={saving || detailsLoading}>
|
||||
<fieldset disabled={!canWrite || saving || !detailsReady} className="flex min-h-0 flex-1 flex-col" aria-busy={saving || detailsLoading}>
|
||||
<Stepper value={sheetStep} onValueChange={setSheetStep} className="flex-1 flex flex-col min-h-0 px-6 py-5">
|
||||
<StepperNav className="mb-5">
|
||||
<StepperItem step={1}>
|
||||
@@ -875,7 +893,7 @@ function ServersPageContent() {
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button type="button" variant="outline" size="sm" className="w-fit gap-2"
|
||||
disabled={testState === "testing"} onClick={handleTest}>
|
||||
disabled={!canWrite || testState === "testing"} onClick={handleTest}>
|
||||
{testState === "testing"
|
||||
? <><LoaderCircleIcon className="size-4 animate-spin" />Проверка…</>
|
||||
: <><WifiIcon className="size-4" />Проверить подключение</>}
|
||||
@@ -936,7 +954,7 @@ function ServersPageContent() {
|
||||
{sheetStep < 4 ? (
|
||||
<Button className="ml-auto" disabled={saving || !detailsReady} onClick={() => setSheetStep((s) => s + 1)}>Далее</Button>
|
||||
) : (
|
||||
<Button className="ml-auto" disabled={saving || !detailsReady || testState === "testing"} onClick={handleSave}>
|
||||
<Button className="ml-auto" disabled={!canWrite || saving || !detailsReady || testState === "testing"} onClick={handleSave}>
|
||||
{saving ? "Сохранение…" : sheetMode === "edit" ? "Сохранить" : "Добавить сервер"}
|
||||
</Button>
|
||||
)}
|
||||
@@ -952,7 +970,7 @@ function ServersPageContent() {
|
||||
{deleteError && <p role="alert" className="text-sm text-destructive">{deleteError}</p>}
|
||||
<AlertDialogFooter>
|
||||
<Button type="button" variant="outline" disabled={deleting} onClick={() => setDeleteTarget(null)}>Отмена</Button>
|
||||
<Button type="button" variant="destructive" disabled={deleting} onClick={() => { if (deleteTarget) void handleDelete(deleteTarget.id) }}>{deleting ? "Удаление…" : "Удалить"}</Button>
|
||||
<Button type="button" variant="destructive" disabled={!canWrite || deleting} onClick={() => { if (deleteTarget) void handleDelete(deleteTarget.id) }}>{deleting ? "Удаление…" : "Удалить"}</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
Reference in new issue
Block a user