feat(rbac): Viewer-Rolle auf alle verbleibenden CRUD-Seiten durchsetzen — v1.1.92

DNS, NTP, ForwardProxy, RoutingRules, Networks (Interfaces + Routes),
IPAddresses, WireGuard (Servers + Clients + Peers), Backups/RemoteTargets, SSL:
Add-Buttons, quickToggle-Switches und Settings-Save-Buttons für Viewer deaktiviert.
ActionButtons-Komponente schützt Edit/Delete bereits zentral; hier wurden nur
die verbleibenden Mutationspunkte (Add, Switch, Submit) nachgezogen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-24 13:56:24 +02:00
parent d4267e2003
commit d3a3b93c1b
15 changed files with 252 additions and 149 deletions

View File

@@ -1,11 +1,12 @@
import { useState } from 'react'
import { Alert, Button, Drawer, Form, Input, InputNumber, Modal, Select, Space, Switch, Tabs, Tag, Typography, message } from 'antd'
import { Alert, Button, Drawer, Form, Input, InputNumber, Modal, Select, Space, Switch, Tabs, Tag, Tooltip, Typography, message } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { CheckCircleOutlined, CloseCircleOutlined, GlobalOutlined, NodeIndexOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import apiClient, { isEnvelope } from '../../api/client'
import { useAuthStore } from '../../stores/auth'
import DataTable from '../../components/DataTable'
import EmptyState from '../../components/EmptyState'
import PageHeader from '../../components/PageHeader'
@@ -120,6 +121,7 @@ export default function DNSPage() {
function ZonesTab() {
const { t } = useTranslation()
const qc = useQueryClient()
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
const { data, isLoading } = useQuery({ queryKey: ['dns', 'zones'], queryFn: listZones })
const [editing, setEditing] = useState<Zone | null>(null)
@@ -167,6 +169,7 @@ function ZonesTab() {
<Switch
size="small"
checked={v}
disabled={isViewer}
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
/>
@@ -207,9 +210,11 @@ function ZonesTab() {
dataSource={data ?? []}
columns={cols}
extraActions={
<Button type="primary" icon={<PlusOutlined />} onClick={openZoneCreate}>
{t('dns.zone.add')}
</Button>
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openZoneCreate}>
{t('dns.zone.add')}
</Button>
</Tooltip>
}
emptyContent={
<EmptyState
@@ -217,9 +222,11 @@ function ZonesTab() {
title={t('dns.zone.emptyTitle')}
description={t('dns.zone.emptyDesc')}
action={
<Button type="primary" icon={<PlusOutlined />} onClick={openZoneCreate}>
{t('dns.zone.add')}
</Button>
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openZoneCreate}>
{t('dns.zone.add')}
</Button>
</Tooltip>
}
/>
}
@@ -277,6 +284,7 @@ interface RecordsDrawerProps {
function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
const { t } = useTranslation()
const qc = useQueryClient()
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
const open = zone !== null
const zoneID = zone?.id ?? 0
@@ -327,6 +335,7 @@ function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
<Switch
size="small"
checked={v}
disabled={isViewer}
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
/>
@@ -371,9 +380,11 @@ function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
dataSource={data ?? []}
columns={cols}
extraActions={
<Button type="primary" icon={<PlusOutlined />} onClick={openRecordCreate}>
{t('dns.record.add')}
</Button>
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openRecordCreate}>
{t('dns.record.add')}
</Button>
</Tooltip>
}
emptyContent={
<EmptyState
@@ -381,9 +392,11 @@ function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
title={t('dns.record.emptyTitle')}
description={t('dns.record.emptyDesc')}
action={
<Button type="primary" icon={<PlusOutlined />} onClick={openRecordCreate}>
{t('dns.record.add')}
</Button>
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openRecordCreate}>
{t('dns.record.add')}
</Button>
</Tooltip>
}
/>
}
@@ -433,6 +446,7 @@ interface SettingsForm extends Omit<Settings, 'listen_addresses'> {
function SettingsTab() {
const { t } = useTranslation()
const qc = useQueryClient()
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
const { data, isLoading } = useQuery({ queryKey: ['dns', 'settings'], queryFn: getSettings })
const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces })
const [form] = Form.useForm<SettingsForm>()
@@ -526,9 +540,11 @@ function SettingsTab() {
</Form.Item>
</Space>
<Form.Item>
<Button type="primary" htmlType="submit" loading={save.isPending}>
{t('common.save')}
</Button>
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button type="primary" htmlType="submit" disabled={isViewer} loading={save.isPending}>
{t('common.save')}
</Button>
</Tooltip>
</Form.Item>
</Form>
)