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

@@ -10,6 +10,7 @@ import DataTable from '../../components/DataTable'
import EmptyState from '../../components/EmptyState'
import apiClient, { isEnvelope } from '../../api/client'
import { useAuthStore } from '../../stores/auth'
interface TLSCert {
id: number
@@ -95,6 +96,7 @@ function relativeAgo(ms: number, t: (k: string, v?: Record<string, unknown>) =>
export default function SSLPage() {
const { t } = useTranslation()
const qc = useQueryClient()
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
const { data: certs, isLoading } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts })
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
@@ -219,20 +221,24 @@ export default function SSLPage() {
render: (_, row) => (
<Space size={4}>
{row.issuer === 'letsencrypt' && (
<Popconfirm
title={t('ssl.renewConfirmTitle')}
description={t('ssl.renewConfirmDesc', { domain: row.domain })}
okText={t('common.yes')} cancelText={t('common.no')}
onConfirm={() => renewMut.mutate(row.domain)}
>
<Button
size="small"
icon={<ReloadOutlined />}
loading={renewMut.isPending && renewMut.variables === row.domain}
>
{t('ssl.renewBtn')}
</Button>
</Popconfirm>
isViewer
? <Tooltip title={t('auth.viewerBadge')}>
<Button size="small" icon={<ReloadOutlined />} disabled>{t('ssl.renewBtn')}</Button>
</Tooltip>
: <Popconfirm
title={t('ssl.renewConfirmTitle')}
description={t('ssl.renewConfirmDesc', { domain: row.domain })}
okText={t('common.yes')} cancelText={t('common.no')}
onConfirm={() => renewMut.mutate(row.domain)}
>
<Button
size="small"
icon={<ReloadOutlined />}
loading={renewMut.isPending && renewMut.variables === row.domain}
>
{t('ssl.renewBtn')}
</Button>
</Popconfirm>
)}
<ActionButtons
onDelete={() => delMut.mutate(row.id)}
@@ -263,9 +269,11 @@ export default function SSLPage() {
allowClear
/>
</Form.Item>
<Button type="primary" htmlType="submit" loading={issueMut.isPending}>
{t('ssl.issueButton')}
</Button>
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button type="primary" htmlType="submit" disabled={isViewer} loading={issueMut.isPending}>
{t('ssl.issueButton')}
</Button>
</Tooltip>
</Form>
</Card>
),
@@ -290,9 +298,11 @@ export default function SSLPage() {
<Input.TextArea rows={6} placeholder="-----BEGIN PRIVATE KEY-----..." style={{ fontFamily: 'monospace', fontSize: 12 }} />
</Form.Item>
<Space>
<Button type="primary" htmlType="submit" loading={uploadMut.isPending}>
{t('ssl.uploadButton')}
</Button>
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
<Button type="primary" htmlType="submit" disabled={isViewer} loading={uploadMut.isPending}>
{t('ssl.uploadButton')}
</Button>
</Tooltip>
<Typography.Text type="secondary">{t('ssl.uploadHint')}</Typography.Text>
</Space>
</Form>