From d3a3b93c1b0b88ae2b741c193916f681bbdaab24 Mon Sep 17 00:00:00 2001 From: Debian Date: Sun, 24 May 2026 13:56:24 +0200 Subject: [PATCH] =?UTF-8?q?feat(rbac):=20Viewer-Rolle=20auf=20alle=20verbl?= =?UTF-8?q?eibenden=20CRUD-Seiten=20durchsetzen=20=E2=80=94=20v1.1.92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- VERSION | 2 +- cmd/edgeguard-api/main.go | 2 +- cmd/edgeguard-ctl/main.go | 2 +- cmd/edgeguard-scheduler/main.go | 2 +- .../src/pages/Backups/RemoteTargets.tsx | 73 +++++++++++-------- management-ui/src/pages/DNS/index.tsx | 48 ++++++++---- .../src/pages/ForwardProxy/index.tsx | 21 ++++-- management-ui/src/pages/IPAddresses/index.tsx | 21 ++++-- management-ui/src/pages/NTP/index.tsx | 28 ++++--- .../src/pages/Networks/Interfaces.tsx | 19 +++-- management-ui/src/pages/Networks/Routes.tsx | 56 ++++++++------ .../src/pages/RoutingRules/index.tsx | 19 +++-- management-ui/src/pages/SSL/index.tsx | 50 ++++++++----- management-ui/src/pages/Wireguard/Clients.tsx | 19 +++-- management-ui/src/pages/Wireguard/Servers.tsx | 39 ++++++---- 15 files changed, 252 insertions(+), 149 deletions(-) diff --git a/VERSION b/VERSION index 069199b..99f6a00 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.91 +1.1.92 diff --git a/cmd/edgeguard-api/main.go b/cmd/edgeguard-api/main.go index cffe9d5..cdcafae 100644 --- a/cmd/edgeguard-api/main.go +++ b/cmd/edgeguard-api/main.go @@ -60,7 +60,7 @@ import ( usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users" ) -var version = "1.1.91" +var version = "1.1.92" func main() { addr := os.Getenv("EDGEGUARD_API_ADDR") diff --git a/cmd/edgeguard-ctl/main.go b/cmd/edgeguard-ctl/main.go index a22ca0e..4723801 100644 --- a/cmd/edgeguard-ctl/main.go +++ b/cmd/edgeguard-ctl/main.go @@ -11,7 +11,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/setup" ) -var version = "1.1.91" +var version = "1.1.92" const usage = `edgeguard-ctl — EdgeGuard CLI diff --git a/cmd/edgeguard-scheduler/main.go b/cmd/edgeguard-scheduler/main.go index f731b5c..a041ed9 100644 --- a/cmd/edgeguard-scheduler/main.go +++ b/cmd/edgeguard-scheduler/main.go @@ -35,7 +35,7 @@ import ( "git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts" ) -var version = "1.1.91" +var version = "1.1.92" const ( // renewTickInterval — how often we re-evaluate expiring certs. diff --git a/management-ui/src/pages/Backups/RemoteTargets.tsx b/management-ui/src/pages/Backups/RemoteTargets.tsx index 2a9d40f..d1c069a 100644 --- a/management-ui/src/pages/Backups/RemoteTargets.tsx +++ b/management-ui/src/pages/Backups/RemoteTargets.tsx @@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next' import dayjs from 'dayjs' import apiClient, { isEnvelope } from '../../api/client' +import { useAuthStore } from '../../stores/auth' const { Text } = Typography @@ -83,6 +84,7 @@ function buildPayload(v: FormValues): RemoteTarget { export default function RemoteTargetsTab() { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const [msg, msgCtx] = message.useMessage() const list = useQuery({ @@ -170,6 +172,7 @@ export default function RemoteTargetsTab() { quickToggle.mutate({ id: r.id, row: r, checked })} /> @@ -184,31 +187,34 @@ export default function RemoteTargetsTab() { onClick={() => test.mutate(r.id)}> {t('remotes.test')} - - del.mutate(r.id)}> - - + + + + {isViewer + ? + : del.mutate(r.id)}> + + } ), }, @@ -225,13 +231,16 @@ export default function RemoteTargetsTab() { description={t('remotes.scopeDesc')} /> } - onClick={() => { - setCreating(true); form.resetFields() - form.setFieldsValue({ kind: 's3', active: true, use_ssl: true, port: 22 }) - }}> - {t('remotes.add')} - + + + }> s.user?.role) === 'viewer' const { data, isLoading } = useQuery({ queryKey: ['dns', 'zones'], queryFn: listZones }) const [editing, setEditing] = useState(null) @@ -167,6 +169,7 @@ function ZonesTab() { quickToggle.mutate({ id: row.id, row, checked })} /> @@ -207,9 +210,11 @@ function ZonesTab() { dataSource={data ?? []} columns={cols} extraActions={ - + + + } emptyContent={ } onClick={openZoneCreate}> - {t('dns.zone.add')} - + + + } /> } @@ -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) { quickToggle.mutate({ id: row.id, row, checked })} /> @@ -371,9 +380,11 @@ function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) { dataSource={data ?? []} columns={cols} extraActions={ - + + + } emptyContent={ } onClick={openRecordCreate}> - {t('dns.record.add')} - + + + } /> } @@ -433,6 +446,7 @@ interface SettingsForm extends Omit { 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() @@ -526,9 +540,11 @@ function SettingsTab() { - + + + ) diff --git a/management-ui/src/pages/ForwardProxy/index.tsx b/management-ui/src/pages/ForwardProxy/index.tsx index c876c06..df848d9 100644 --- a/management-ui/src/pages/ForwardProxy/index.tsx +++ b/management-ui/src/pages/ForwardProxy/index.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { - Alert, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, Typography, message, + Alert, Button, Form, Input, InputNumber, Modal, Select, Space, Switch, Tag, Tooltip, Typography, message, } from 'antd' import type { ColumnsType } from 'antd/es/table' import { CheckCircleOutlined, CloseCircleOutlined, CloudServerOutlined, PlusOutlined } from '@ant-design/icons' @@ -8,6 +8,7 @@ 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' @@ -54,6 +55,7 @@ async function listACLs(): Promise { export default function ForwardProxyPage() { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const { data, isLoading } = useQuery({ queryKey: ['fwd-proxy', 'acls'], queryFn: listACLs }) const { data: services } = useQuery({ @@ -112,6 +114,7 @@ export default function ForwardProxyPage() { quickToggle.mutate({ id: row.id, row, checked })} /> @@ -174,9 +177,11 @@ export default function ForwardProxyPage() { dataSource={data ?? []} columns={cols} extraActions={ - + + + } emptyContent={ } onClick={openCreate}> - {t('fwd.add')} - + + + } /> } diff --git a/management-ui/src/pages/IPAddresses/index.tsx b/management-ui/src/pages/IPAddresses/index.tsx index bfd81bd..2c9186b 100644 --- a/management-ui/src/pages/IPAddresses/index.tsx +++ b/management-ui/src/pages/IPAddresses/index.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Button, Card, Form, Input, InputNumber, Modal, Select, Switch, Tag, Typography, message } from 'antd' +import { Button, Card, Form, Input, InputNumber, Modal, Select, Switch, Tag, Tooltip, Typography, message } from 'antd' import { NodeIndexOutlined, PlusOutlined } from '@ant-design/icons' import PageHeader from '../../components/PageHeader' import ActionButtons from '../../components/ActionButtons' @@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next' import DataTable from '../../components/DataTable' import apiClient, { isEnvelope } from '../../api/client' +import { useAuthStore } from '../../stores/auth' interface IPAddress { id: number @@ -85,6 +86,7 @@ async function listSystemAddresses(): Promise { export default function IPAddressesPage() { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const { data: ips, isLoading } = useQuery({ queryKey: ['ip-addresses'], queryFn: listIPs }) const { data: ifs } = useQuery({ queryKey: ['network-interfaces'], queryFn: listIfaces }) @@ -150,6 +152,7 @@ export default function IPAddressesPage() { quickToggle.mutate({ id: row.id, row, checked })} /> @@ -216,9 +219,11 @@ export default function IPAddressesPage() { dataSource={ips ?? []} columns={columns} extraActions={ - + + + } emptyContent={ } onClick={openCreate}> - {t('ips.addAddress')} - + + + } /> } diff --git a/management-ui/src/pages/NTP/index.tsx b/management-ui/src/pages/NTP/index.tsx index 27f45ee..cf3f2a5 100644 --- a/management-ui/src/pages/NTP/index.tsx +++ b/management-ui/src/pages/NTP/index.tsx @@ -8,6 +8,7 @@ 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' @@ -180,6 +181,7 @@ export default function NTPPage() { function PoolsTab() { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const { data, isLoading } = useQuery({ queryKey: ['ntp', 'pools'], queryFn: listPools }) const [editing, setEditing] = useState(null) @@ -234,6 +236,7 @@ function PoolsTab() { quickToggle.mutate({ id: row.id, row, checked })} /> @@ -267,9 +270,11 @@ function PoolsTab() { dataSource={data ?? []} columns={cols} extraActions={ - + + + } emptyContent={ } onClick={openCreate}> - {t('ntp.pool.add')} - + + + } /> } @@ -336,6 +343,7 @@ function PoolsTab() { function SettingsTab() { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const { data, isLoading } = useQuery({ queryKey: ['ntp', 'settings'], queryFn: getSettings }) const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces }) const [form] = Form.useForm() @@ -415,9 +423,11 @@ function SettingsTab() { - + + + ) diff --git a/management-ui/src/pages/Networks/Interfaces.tsx b/management-ui/src/pages/Networks/Interfaces.tsx index 265386a..b7781b0 100644 --- a/management-ui/src/pages/Networks/Interfaces.tsx +++ b/management-ui/src/pages/Networks/Interfaces.tsx @@ -9,6 +9,7 @@ import EmptyState from '../../components/EmptyState' import ActionButtons from '../../components/ActionButtons' import apiClient, { isEnvelope } from '../../api/client' +import { useAuthStore } from '../../stores/auth' interface NetworkInterface { id: number @@ -77,6 +78,7 @@ async function listZones(): Promise { export default function InterfacesTab() { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const { data: ifs, isLoading } = useQuery({ queryKey: ['network-interfaces'], queryFn: listInterfaces }) const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces, refetchInterval: 10_000 }) @@ -151,6 +153,7 @@ export default function InterfacesTab() { quickToggle.mutate({ id: row.id, row, checked })} /> @@ -235,9 +238,11 @@ export default function InterfacesTab() { dataSource={ifs ?? []} columns={columns} extraActions={ - + + + } emptyContent={ } onClick={openCreate}> - {t('networks.addInterface')} - + + + } /> } diff --git a/management-ui/src/pages/Networks/Routes.tsx b/management-ui/src/pages/Networks/Routes.tsx index b937e6a..5a028a4 100644 --- a/management-ui/src/pages/Networks/Routes.tsx +++ b/management-ui/src/pages/Networks/Routes.tsx @@ -10,6 +10,7 @@ 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 EmptyState from '../../components/EmptyState' const { Text } = Typography @@ -50,6 +51,7 @@ interface RouteFormValues { export default function RoutesTab() { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const managed = useQuery({ queryKey: ['routes', 'managed'], @@ -129,6 +131,7 @@ export default function RoutesTab() { quickToggle.mutate({ id: row.id, row, checked })} /> @@ -140,22 +143,25 @@ export default function RoutesTab() { title: t('common.actions'), key: 'a', width: 160, render: (_, r) => ( - - del.mutate(r.id)}> - - + + + + {isViewer + ? + : del.mutate(r.id)}> + + } ), }, @@ -232,10 +238,12 @@ export default function RoutesTab() { size="small" title={t('routes.managedTitle')} extra={ - + + + } > {t('routes.managedIntro')} @@ -252,9 +260,11 @@ export default function RoutesTab() { title={t('routes.emptyTitle')} description={t('routes.emptyDesc')} action={ - + + + } /> ) }} diff --git a/management-ui/src/pages/RoutingRules/index.tsx b/management-ui/src/pages/RoutingRules/index.tsx index 08852e0..18eb321 100644 --- a/management-ui/src/pages/RoutingRules/index.tsx +++ b/management-ui/src/pages/RoutingRules/index.tsx @@ -10,6 +10,7 @@ import PageHeader from '../../components/PageHeader' import ActionButtons from '../../components/ActionButtons' import apiClient, { isEnvelope } from '../../api/client' +import { useAuthStore } from '../../stores/auth' interface RoutingRule { id: number @@ -68,6 +69,7 @@ async function listHAProxyStats(): Promise { export default function RoutingRulesPage() { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const { data: rules, isLoading } = useQuery({ queryKey: ['routing-rules'], queryFn: listRules }) const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains }) @@ -160,6 +162,7 @@ export default function RoutingRulesPage() { quickToggle.mutate({ id: row.id, row, checked })} /> @@ -204,9 +207,11 @@ export default function RoutingRulesPage() { dataSource={rules ?? []} columns={columns} extraActions={ - + + + } emptyContent={ } onClick={openCreate}> - {t('routing.addRule')} - + + + } /> } diff --git a/management-ui/src/pages/SSL/index.tsx b/management-ui/src/pages/SSL/index.tsx index 1a181c1..53a43b7 100644 --- a/management-ui/src/pages/SSL/index.tsx +++ b/management-ui/src/pages/SSL/index.tsx @@ -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) => 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) => ( {row.issuer === 'letsencrypt' && ( - renewMut.mutate(row.domain)} - > - - + isViewer + ? + + + : renewMut.mutate(row.domain)} + > + + )} delMut.mutate(row.id)} @@ -263,9 +269,11 @@ export default function SSLPage() { allowClear /> - + + + ), @@ -290,9 +298,11 @@ export default function SSLPage() { - + + + {t('ssl.uploadHint')} diff --git a/management-ui/src/pages/Wireguard/Clients.tsx b/management-ui/src/pages/Wireguard/Clients.tsx index 7d5d174..0ba99b0 100644 --- a/management-ui/src/pages/Wireguard/Clients.tsx +++ b/management-ui/src/pages/Wireguard/Clients.tsx @@ -9,6 +9,7 @@ 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 ActionButtons from '../../components/ActionButtons' @@ -81,6 +82,7 @@ async function listZones(): Promise { export default function ClientsTab() { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const { data: clients, isLoading } = useQuery({ queryKey: ['wg', 'clients'], queryFn: listClients }) const { data: zones } = useQuery({ queryKey: ['fw-zones'], queryFn: listZones }) @@ -160,6 +162,7 @@ export default function ClientsTab() { quickToggle.mutate({ id: row.id, row, checked })} /> @@ -214,9 +217,11 @@ export default function ClientsTab() { dataSource={clients ?? []} columns={cols} extraActions={ - + + + } emptyContent={ } onClick={openCreate}> - {t('wg.iface.addClient')} - + + + } /> } diff --git a/management-ui/src/pages/Wireguard/Servers.tsx b/management-ui/src/pages/Wireguard/Servers.tsx index 8c305ae..05a1875 100644 --- a/management-ui/src/pages/Wireguard/Servers.tsx +++ b/management-ui/src/pages/Wireguard/Servers.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { Alert, Button, Card, Col, Drawer, Form, Input, InputNumber, - Modal, Row, Select, Space, Switch, Tag, Typography, message, + Modal, Row, Select, Space, Switch, Tag, Tooltip, Typography, message, } from 'antd' import type { ColumnsType } from 'antd/es/table' import { @@ -12,6 +12,7 @@ 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 ActionButtons from '../../components/ActionButtons' @@ -98,6 +99,7 @@ async function listZones(): Promise { export default function ServersTab() { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const { data: servers, isLoading } = useQuery({ queryKey: ['wg', 'servers'], queryFn: listServers }) const { data: zones } = useQuery({ queryKey: ['fw-zones'], queryFn: listZones }) @@ -152,6 +154,7 @@ export default function ServersTab() { quickToggle.mutate({ id: row.id, row, checked })} /> @@ -208,9 +211,11 @@ export default function ServersTab() { dataSource={servers ?? []} columns={cols} extraActions={ - + + + } emptyContent={ } onClick={openCreate}> - {t('wg.iface.addServer')} - + + + } /> } @@ -333,6 +340,7 @@ interface PeerDrawerProps { function PeerDrawer({ iface, onClose }: PeerDrawerProps) { const { t } = useTranslation() const qc = useQueryClient() + const isViewer = useAuthStore((s) => s.user?.role) === 'viewer' const open = iface !== null const ifaceID = iface?.id ?? 0 @@ -432,6 +440,7 @@ function PeerDrawer({ iface, onClose }: PeerDrawerProps) { peerToggle.mutate({ id: row.id, row, checked })} /> @@ -504,9 +513,11 @@ function PeerDrawer({ iface, onClose }: PeerDrawerProps) { dataSource={peers ?? []} columns={cols} extraActions={ - + + + } emptyContent={ } onClick={openPeerCreate}> - {t('wg.peer.add')} - + + + } /> }