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:
@@ -60,7 +60,7 @@ import (
|
|||||||
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.1.91"
|
var version = "1.1.92"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
addr := os.Getenv("EDGEGUARD_API_ADDR")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
"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
|
const usage = `edgeguard-ctl — EdgeGuard CLI
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import (
|
|||||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "1.1.91"
|
var version = "1.1.92"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// renewTickInterval — how often we re-evaluate expiring certs.
|
// renewTickInterval — how often we re-evaluate expiring certs.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
@@ -83,6 +84,7 @@ function buildPayload(v: FormValues): RemoteTarget {
|
|||||||
export default function RemoteTargetsTab() {
|
export default function RemoteTargetsTab() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
const [msg, msgCtx] = message.useMessage()
|
const [msg, msgCtx] = message.useMessage()
|
||||||
|
|
||||||
const list = useQuery({
|
const list = useQuery({
|
||||||
@@ -170,6 +172,7 @@ export default function RemoteTargetsTab() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === r.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === r.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: r.id, row: r, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: r.id, row: r, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -184,31 +187,34 @@ export default function RemoteTargetsTab() {
|
|||||||
onClick={() => test.mutate(r.id)}>
|
onClick={() => test.mutate(r.id)}>
|
||||||
{t('remotes.test')}
|
{t('remotes.test')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="small" onClick={() => {
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
setEdit(r); form.resetFields()
|
<Button size="small" disabled={isViewer} onClick={() => {
|
||||||
const settings = (r.settings ?? {}) as Record<string, unknown>
|
setEdit(r); form.resetFields()
|
||||||
form.setFieldsValue({
|
const settings = (r.settings ?? {}) as Record<string, unknown>
|
||||||
name: r.name, kind: r.kind, target_url: r.target_url, active: r.active,
|
form.setFieldsValue({
|
||||||
endpoint: settings.endpoint as string,
|
name: r.name, kind: r.kind, target_url: r.target_url, active: r.active,
|
||||||
region: settings.region as string,
|
endpoint: settings.endpoint as string,
|
||||||
bucket: settings.bucket as string,
|
region: settings.region as string,
|
||||||
access_key: settings.access_key as string,
|
bucket: settings.bucket as string,
|
||||||
secret_key: settings.secret_key as string,
|
access_key: settings.access_key as string,
|
||||||
path_prefix: settings.path_prefix as string,
|
secret_key: settings.secret_key as string,
|
||||||
use_ssl: settings.use_ssl as boolean,
|
path_prefix: settings.path_prefix as string,
|
||||||
host: settings.host as string,
|
use_ssl: settings.use_ssl as boolean,
|
||||||
port: settings.port as number,
|
host: settings.host as string,
|
||||||
username: settings.username as string,
|
port: settings.port as number,
|
||||||
password: settings.password as string,
|
username: settings.username as string,
|
||||||
private_key: settings.private_key as string,
|
password: settings.password as string,
|
||||||
remote_dir: settings.remote_dir as string,
|
private_key: settings.private_key as string,
|
||||||
host_key_fingerprint: settings.host_key_fingerprint as string,
|
remote_dir: settings.remote_dir as string,
|
||||||
})
|
host_key_fingerprint: settings.host_key_fingerprint as string,
|
||||||
}}>{t('common.edit')}</Button>
|
})
|
||||||
<Popconfirm title={t('remotes.confirmDelete', { name: r.name })}
|
}}>{t('common.edit')}</Button>
|
||||||
onConfirm={() => del.mutate(r.id)}>
|
</Tooltip>
|
||||||
<Button size="small" danger>{t('common.delete')}</Button>
|
{isViewer
|
||||||
</Popconfirm>
|
? <Tooltip title={t('auth.viewerBadge')}><Button size="small" danger disabled>{t('common.delete')}</Button></Tooltip>
|
||||||
|
: <Popconfirm title={t('remotes.confirmDelete', { name: r.name })} onConfirm={() => del.mutate(r.id)}>
|
||||||
|
<Button size="small" danger>{t('common.delete')}</Button>
|
||||||
|
</Popconfirm>}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -225,13 +231,16 @@ export default function RemoteTargetsTab() {
|
|||||||
description={t('remotes.scopeDesc')}
|
description={t('remotes.scopeDesc')}
|
||||||
/>
|
/>
|
||||||
<Card size="small" extra={
|
<Card size="small" extra={
|
||||||
<Button type="primary" size="small" icon={<PlusOutlined />}
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
onClick={() => {
|
<Button type="primary" size="small" icon={<PlusOutlined />}
|
||||||
setCreating(true); form.resetFields()
|
disabled={isViewer}
|
||||||
form.setFieldsValue({ kind: 's3', active: true, use_ssl: true, port: 22 })
|
onClick={() => {
|
||||||
}}>
|
setCreating(true); form.resetFields()
|
||||||
{t('remotes.add')}
|
form.setFieldsValue({ kind: 's3', active: true, use_ssl: true, port: 22 })
|
||||||
</Button>
|
}}>
|
||||||
|
{t('remotes.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}>
|
}>
|
||||||
<Table size="small" rowKey="id" loading={list.isFetching}
|
<Table size="small" rowKey="id" loading={list.isFetching}
|
||||||
dataSource={list.data ?? []} columns={columns}
|
dataSource={list.data ?? []} columns={columns}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { useState } from 'react'
|
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 type { ColumnsType } from 'antd/es/table'
|
||||||
import { CheckCircleOutlined, CloseCircleOutlined, GlobalOutlined, NodeIndexOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons'
|
import { CheckCircleOutlined, CloseCircleOutlined, GlobalOutlined, NodeIndexOutlined, PlusOutlined, SettingOutlined } from '@ant-design/icons'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import DataTable from '../../components/DataTable'
|
import DataTable from '../../components/DataTable'
|
||||||
import EmptyState from '../../components/EmptyState'
|
import EmptyState from '../../components/EmptyState'
|
||||||
import PageHeader from '../../components/PageHeader'
|
import PageHeader from '../../components/PageHeader'
|
||||||
@@ -120,6 +121,7 @@ export default function DNSPage() {
|
|||||||
function ZonesTab() {
|
function ZonesTab() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
const { data, isLoading } = useQuery({ queryKey: ['dns', 'zones'], queryFn: listZones })
|
const { data, isLoading } = useQuery({ queryKey: ['dns', 'zones'], queryFn: listZones })
|
||||||
|
|
||||||
const [editing, setEditing] = useState<Zone | null>(null)
|
const [editing, setEditing] = useState<Zone | null>(null)
|
||||||
@@ -167,6 +169,7 @@ function ZonesTab() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -207,9 +210,11 @@ function ZonesTab() {
|
|||||||
dataSource={data ?? []}
|
dataSource={data ?? []}
|
||||||
columns={cols}
|
columns={cols}
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openZoneCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('dns.zone.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openZoneCreate}>
|
||||||
</Button>
|
{t('dns.zone.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
emptyContent={
|
emptyContent={
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -217,9 +222,11 @@ function ZonesTab() {
|
|||||||
title={t('dns.zone.emptyTitle')}
|
title={t('dns.zone.emptyTitle')}
|
||||||
description={t('dns.zone.emptyDesc')}
|
description={t('dns.zone.emptyDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openZoneCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('dns.zone.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openZoneCreate}>
|
||||||
</Button>
|
{t('dns.zone.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -277,6 +284,7 @@ interface RecordsDrawerProps {
|
|||||||
function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
|
function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
const open = zone !== null
|
const open = zone !== null
|
||||||
const zoneID = zone?.id ?? 0
|
const zoneID = zone?.id ?? 0
|
||||||
|
|
||||||
@@ -327,6 +335,7 @@ function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -371,9 +380,11 @@ function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
|
|||||||
dataSource={data ?? []}
|
dataSource={data ?? []}
|
||||||
columns={cols}
|
columns={cols}
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openRecordCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('dns.record.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openRecordCreate}>
|
||||||
</Button>
|
{t('dns.record.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
emptyContent={
|
emptyContent={
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -381,9 +392,11 @@ function RecordsDrawer({ zone, onClose }: RecordsDrawerProps) {
|
|||||||
title={t('dns.record.emptyTitle')}
|
title={t('dns.record.emptyTitle')}
|
||||||
description={t('dns.record.emptyDesc')}
|
description={t('dns.record.emptyDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openRecordCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('dns.record.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openRecordCreate}>
|
||||||
</Button>
|
{t('dns.record.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -433,6 +446,7 @@ interface SettingsForm extends Omit<Settings, 'listen_addresses'> {
|
|||||||
function SettingsTab() {
|
function SettingsTab() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
const { data, isLoading } = useQuery({ queryKey: ['dns', 'settings'], queryFn: getSettings })
|
const { data, isLoading } = useQuery({ queryKey: ['dns', 'settings'], queryFn: getSettings })
|
||||||
const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces })
|
const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces })
|
||||||
const [form] = Form.useForm<SettingsForm>()
|
const [form] = Form.useForm<SettingsForm>()
|
||||||
@@ -526,9 +540,11 @@ function SettingsTab() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary" htmlType="submit" loading={save.isPending}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('common.save')}
|
<Button type="primary" htmlType="submit" disabled={isViewer} loading={save.isPending}>
|
||||||
</Button>
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import {
|
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'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import { CheckCircleOutlined, CloseCircleOutlined, CloudServerOutlined, PlusOutlined } from '@ant-design/icons'
|
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 { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import DataTable from '../../components/DataTable'
|
import DataTable from '../../components/DataTable'
|
||||||
import EmptyState from '../../components/EmptyState'
|
import EmptyState from '../../components/EmptyState'
|
||||||
import PageHeader from '../../components/PageHeader'
|
import PageHeader from '../../components/PageHeader'
|
||||||
@@ -54,6 +55,7 @@ async function listACLs(): Promise<ACL[]> {
|
|||||||
export default function ForwardProxyPage() {
|
export default function ForwardProxyPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
const { data, isLoading } = useQuery({ queryKey: ['fwd-proxy', 'acls'], queryFn: listACLs })
|
const { data, isLoading } = useQuery({ queryKey: ['fwd-proxy', 'acls'], queryFn: listACLs })
|
||||||
|
|
||||||
const { data: services } = useQuery({
|
const { data: services } = useQuery({
|
||||||
@@ -112,6 +114,7 @@ export default function ForwardProxyPage() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -174,9 +177,11 @@ export default function ForwardProxyPage() {
|
|||||||
dataSource={data ?? []}
|
dataSource={data ?? []}
|
||||||
columns={cols}
|
columns={cols}
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('fwd.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('fwd.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
emptyContent={
|
emptyContent={
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -184,9 +189,11 @@ export default function ForwardProxyPage() {
|
|||||||
title={t('fwd.emptyTitle')}
|
title={t('fwd.emptyTitle')}
|
||||||
description={t('fwd.emptyDesc')}
|
description={t('fwd.emptyDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('fwd.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('fwd.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
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 { NodeIndexOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
import PageHeader from '../../components/PageHeader'
|
import PageHeader from '../../components/PageHeader'
|
||||||
import ActionButtons from '../../components/ActionButtons'
|
import ActionButtons from '../../components/ActionButtons'
|
||||||
@@ -10,6 +10,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import DataTable from '../../components/DataTable'
|
import DataTable from '../../components/DataTable'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
|
||||||
interface IPAddress {
|
interface IPAddress {
|
||||||
id: number
|
id: number
|
||||||
@@ -85,6 +86,7 @@ async function listSystemAddresses(): Promise<SystemAddress[]> {
|
|||||||
export default function IPAddressesPage() {
|
export default function IPAddressesPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
|
|
||||||
const { data: ips, isLoading } = useQuery({ queryKey: ['ip-addresses'], queryFn: listIPs })
|
const { data: ips, isLoading } = useQuery({ queryKey: ['ip-addresses'], queryFn: listIPs })
|
||||||
const { data: ifs } = useQuery({ queryKey: ['network-interfaces'], queryFn: listIfaces })
|
const { data: ifs } = useQuery({ queryKey: ['network-interfaces'], queryFn: listIfaces })
|
||||||
@@ -150,6 +152,7 @@ export default function IPAddressesPage() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -216,9 +219,11 @@ export default function IPAddressesPage() {
|
|||||||
dataSource={ips ?? []}
|
dataSource={ips ?? []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('ips.addAddress')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('ips.addAddress')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
emptyContent={
|
emptyContent={
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -226,9 +231,11 @@ export default function IPAddressesPage() {
|
|||||||
title={t('ips.emptyTitle')}
|
title={t('ips.emptyTitle')}
|
||||||
description={t('ips.emptyDesc')}
|
description={t('ips.emptyDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('ips.addAddress')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('ips.addAddress')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import DataTable from '../../components/DataTable'
|
import DataTable from '../../components/DataTable'
|
||||||
import EmptyState from '../../components/EmptyState'
|
import EmptyState from '../../components/EmptyState'
|
||||||
import PageHeader from '../../components/PageHeader'
|
import PageHeader from '../../components/PageHeader'
|
||||||
@@ -180,6 +181,7 @@ export default function NTPPage() {
|
|||||||
function PoolsTab() {
|
function PoolsTab() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
const { data, isLoading } = useQuery({ queryKey: ['ntp', 'pools'], queryFn: listPools })
|
const { data, isLoading } = useQuery({ queryKey: ['ntp', 'pools'], queryFn: listPools })
|
||||||
|
|
||||||
const [editing, setEditing] = useState<Pool | null>(null)
|
const [editing, setEditing] = useState<Pool | null>(null)
|
||||||
@@ -234,6 +236,7 @@ function PoolsTab() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -267,9 +270,11 @@ function PoolsTab() {
|
|||||||
dataSource={data ?? []}
|
dataSource={data ?? []}
|
||||||
columns={cols}
|
columns={cols}
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('ntp.pool.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('ntp.pool.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
emptyContent={
|
emptyContent={
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -277,9 +282,11 @@ function PoolsTab() {
|
|||||||
title={t('ntp.pool.emptyTitle')}
|
title={t('ntp.pool.emptyTitle')}
|
||||||
description={t('ntp.pool.emptyDesc')}
|
description={t('ntp.pool.emptyDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('ntp.pool.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('ntp.pool.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -336,6 +343,7 @@ function PoolsTab() {
|
|||||||
function SettingsTab() {
|
function SettingsTab() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
const { data, isLoading } = useQuery({ queryKey: ['ntp', 'settings'], queryFn: getSettings })
|
const { data, isLoading } = useQuery({ queryKey: ['ntp', 'settings'], queryFn: getSettings })
|
||||||
const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces })
|
const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces })
|
||||||
const [form] = Form.useForm<SettingsForm>()
|
const [form] = Form.useForm<SettingsForm>()
|
||||||
@@ -415,9 +423,11 @@ function SettingsTab() {
|
|||||||
<Input placeholder="right/UTC" allowClear />
|
<Input placeholder="right/UTC" allowClear />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button type="primary" htmlType="submit" loading={save.isPending}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('common.save')}
|
<Button type="primary" htmlType="submit" disabled={isViewer} loading={save.isPending}>
|
||||||
</Button>
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import EmptyState from '../../components/EmptyState'
|
|||||||
import ActionButtons from '../../components/ActionButtons'
|
import ActionButtons from '../../components/ActionButtons'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
|
||||||
interface NetworkInterface {
|
interface NetworkInterface {
|
||||||
id: number
|
id: number
|
||||||
@@ -77,6 +78,7 @@ async function listZones(): Promise<FwZone[]> {
|
|||||||
export default function InterfacesTab() {
|
export default function InterfacesTab() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
|
|
||||||
const { data: ifs, isLoading } = useQuery({ queryKey: ['network-interfaces'], queryFn: listInterfaces })
|
const { data: ifs, isLoading } = useQuery({ queryKey: ['network-interfaces'], queryFn: listInterfaces })
|
||||||
const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces, refetchInterval: 10_000 })
|
const { data: sys } = useQuery({ queryKey: ['system', 'interfaces'], queryFn: listSystemInterfaces, refetchInterval: 10_000 })
|
||||||
@@ -151,6 +153,7 @@ export default function InterfacesTab() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -235,9 +238,11 @@ export default function InterfacesTab() {
|
|||||||
dataSource={ifs ?? []}
|
dataSource={ifs ?? []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('networks.addInterface')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('networks.addInterface')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
emptyContent={
|
emptyContent={
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -245,9 +250,11 @@ export default function InterfacesTab() {
|
|||||||
title={t('networks.emptyTitle')}
|
title={t('networks.emptyTitle')}
|
||||||
description={t('networks.emptyDesc')}
|
description={t('networks.emptyDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('networks.addInterface')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('networks.addInterface')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import EmptyState from '../../components/EmptyState'
|
import EmptyState from '../../components/EmptyState'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
@@ -50,6 +51,7 @@ interface RouteFormValues {
|
|||||||
export default function RoutesTab() {
|
export default function RoutesTab() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
|
|
||||||
const managed = useQuery({
|
const managed = useQuery({
|
||||||
queryKey: ['routes', 'managed'],
|
queryKey: ['routes', 'managed'],
|
||||||
@@ -129,6 +131,7 @@ export default function RoutesTab() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -140,22 +143,25 @@ export default function RoutesTab() {
|
|||||||
title: t('common.actions'), key: 'a', width: 160,
|
title: t('common.actions'), key: 'a', width: 160,
|
||||||
render: (_, r) => (
|
render: (_, r) => (
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<Button size="small" onClick={() => {
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
setEdit(r)
|
<Button size="small" disabled={isViewer} onClick={() => {
|
||||||
form.setFieldsValue({
|
setEdit(r)
|
||||||
destination: r.destination,
|
form.setFieldsValue({
|
||||||
gateway: r.gateway ?? undefined,
|
destination: r.destination,
|
||||||
dev: r.dev ?? undefined,
|
gateway: r.gateway ?? undefined,
|
||||||
metric: r.metric,
|
dev: r.dev ?? undefined,
|
||||||
table_name: r.table_name,
|
metric: r.metric,
|
||||||
active: r.active,
|
table_name: r.table_name,
|
||||||
comment: r.comment ?? undefined,
|
active: r.active,
|
||||||
})
|
comment: r.comment ?? undefined,
|
||||||
}}>{t('common.edit')}</Button>
|
})
|
||||||
<Popconfirm title={t('routes.confirmDelete', { dest: r.destination })}
|
}}>{t('common.edit')}</Button>
|
||||||
onConfirm={() => del.mutate(r.id)}>
|
</Tooltip>
|
||||||
<Button size="small" danger>{t('common.delete')}</Button>
|
{isViewer
|
||||||
</Popconfirm>
|
? <Tooltip title={t('auth.viewerBadge')}><Button size="small" danger disabled>{t('common.delete')}</Button></Tooltip>
|
||||||
|
: <Popconfirm title={t('routes.confirmDelete', { dest: r.destination })} onConfirm={() => del.mutate(r.id)}>
|
||||||
|
<Button size="small" danger>{t('common.delete')}</Button>
|
||||||
|
</Popconfirm>}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -232,10 +238,12 @@ export default function RoutesTab() {
|
|||||||
size="small"
|
size="small"
|
||||||
title={t('routes.managedTitle')}
|
title={t('routes.managedTitle')}
|
||||||
extra={
|
extra={
|
||||||
<Button type="primary" size="small" icon={<PlusOutlined />}
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
onClick={openCreate}>
|
<Button type="primary" size="small" icon={<PlusOutlined />}
|
||||||
{t('routes.add')}
|
disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('routes.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text type="secondary">{t('routes.managedIntro')}</Text>
|
<Text type="secondary">{t('routes.managedIntro')}</Text>
|
||||||
@@ -252,9 +260,11 @@ export default function RoutesTab() {
|
|||||||
title={t('routes.emptyTitle')}
|
title={t('routes.emptyTitle')}
|
||||||
description={t('routes.emptyDesc')}
|
description={t('routes.emptyDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('routes.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('routes.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) }}
|
) }}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import PageHeader from '../../components/PageHeader'
|
|||||||
import ActionButtons from '../../components/ActionButtons'
|
import ActionButtons from '../../components/ActionButtons'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
|
||||||
interface RoutingRule {
|
interface RoutingRule {
|
||||||
id: number
|
id: number
|
||||||
@@ -68,6 +69,7 @@ async function listHAProxyStats(): Promise<HAProxyStat[]> {
|
|||||||
export default function RoutingRulesPage() {
|
export default function RoutingRulesPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
|
|
||||||
const { data: rules, isLoading } = useQuery({ queryKey: ['routing-rules'], queryFn: listRules })
|
const { data: rules, isLoading } = useQuery({ queryKey: ['routing-rules'], queryFn: listRules })
|
||||||
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
|
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
|
||||||
@@ -160,6 +162,7 @@ export default function RoutingRulesPage() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -204,9 +207,11 @@ export default function RoutingRulesPage() {
|
|||||||
dataSource={rules ?? []}
|
dataSource={rules ?? []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('routing.addRule')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('routing.addRule')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
emptyContent={
|
emptyContent={
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -214,9 +219,11 @@ export default function RoutingRulesPage() {
|
|||||||
title={t('routing.emptyTitle')}
|
title={t('routing.emptyTitle')}
|
||||||
description={t('routing.emptyDesc')}
|
description={t('routing.emptyDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('routing.addRule')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('routing.addRule')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import DataTable from '../../components/DataTable'
|
|||||||
import EmptyState from '../../components/EmptyState'
|
import EmptyState from '../../components/EmptyState'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
|
|
||||||
interface TLSCert {
|
interface TLSCert {
|
||||||
id: number
|
id: number
|
||||||
@@ -95,6 +96,7 @@ function relativeAgo(ms: number, t: (k: string, v?: Record<string, unknown>) =>
|
|||||||
export default function SSLPage() {
|
export default function SSLPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
|
|
||||||
const { data: certs, isLoading } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts })
|
const { data: certs, isLoading } = useQuery({ queryKey: ['tls-certs'], queryFn: listCerts })
|
||||||
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
|
const { data: domains } = useQuery({ queryKey: ['domains'], queryFn: listDomains })
|
||||||
@@ -219,20 +221,24 @@ export default function SSLPage() {
|
|||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
{row.issuer === 'letsencrypt' && (
|
{row.issuer === 'letsencrypt' && (
|
||||||
<Popconfirm
|
isViewer
|
||||||
title={t('ssl.renewConfirmTitle')}
|
? <Tooltip title={t('auth.viewerBadge')}>
|
||||||
description={t('ssl.renewConfirmDesc', { domain: row.domain })}
|
<Button size="small" icon={<ReloadOutlined />} disabled>{t('ssl.renewBtn')}</Button>
|
||||||
okText={t('common.yes')} cancelText={t('common.no')}
|
</Tooltip>
|
||||||
onConfirm={() => renewMut.mutate(row.domain)}
|
: <Popconfirm
|
||||||
>
|
title={t('ssl.renewConfirmTitle')}
|
||||||
<Button
|
description={t('ssl.renewConfirmDesc', { domain: row.domain })}
|
||||||
size="small"
|
okText={t('common.yes')} cancelText={t('common.no')}
|
||||||
icon={<ReloadOutlined />}
|
onConfirm={() => renewMut.mutate(row.domain)}
|
||||||
loading={renewMut.isPending && renewMut.variables === row.domain}
|
>
|
||||||
>
|
<Button
|
||||||
{t('ssl.renewBtn')}
|
size="small"
|
||||||
</Button>
|
icon={<ReloadOutlined />}
|
||||||
</Popconfirm>
|
loading={renewMut.isPending && renewMut.variables === row.domain}
|
||||||
|
>
|
||||||
|
{t('ssl.renewBtn')}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
<ActionButtons
|
<ActionButtons
|
||||||
onDelete={() => delMut.mutate(row.id)}
|
onDelete={() => delMut.mutate(row.id)}
|
||||||
@@ -263,9 +269,11 @@ export default function SSLPage() {
|
|||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit" loading={issueMut.isPending}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('ssl.issueButton')}
|
<Button type="primary" htmlType="submit" disabled={isViewer} loading={issueMut.isPending}>
|
||||||
</Button>
|
{t('ssl.issueButton')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
),
|
),
|
||||||
@@ -290,9 +298,11 @@ export default function SSLPage() {
|
|||||||
<Input.TextArea rows={6} placeholder="-----BEGIN PRIVATE KEY-----..." style={{ fontFamily: 'monospace', fontSize: 12 }} />
|
<Input.TextArea rows={6} placeholder="-----BEGIN PRIVATE KEY-----..." style={{ fontFamily: 'monospace', fontSize: 12 }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="primary" htmlType="submit" loading={uploadMut.isPending}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('ssl.uploadButton')}
|
<Button type="primary" htmlType="submit" disabled={isViewer} loading={uploadMut.isPending}>
|
||||||
</Button>
|
{t('ssl.uploadButton')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
<Typography.Text type="secondary">{t('ssl.uploadHint')}</Typography.Text>
|
<Typography.Text type="secondary">{t('ssl.uploadHint')}</Typography.Text>
|
||||||
</Space>
|
</Space>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import DataTable from '../../components/DataTable'
|
import DataTable from '../../components/DataTable'
|
||||||
import EmptyState from '../../components/EmptyState'
|
import EmptyState from '../../components/EmptyState'
|
||||||
import ActionButtons from '../../components/ActionButtons'
|
import ActionButtons from '../../components/ActionButtons'
|
||||||
@@ -81,6 +82,7 @@ async function listZones(): Promise<FwZoneLite[]> {
|
|||||||
export default function ClientsTab() {
|
export default function ClientsTab() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
|
|
||||||
const { data: clients, isLoading } = useQuery({ queryKey: ['wg', 'clients'], queryFn: listClients })
|
const { data: clients, isLoading } = useQuery({ queryKey: ['wg', 'clients'], queryFn: listClients })
|
||||||
const { data: zones } = useQuery({ queryKey: ['fw-zones'], queryFn: listZones })
|
const { data: zones } = useQuery({ queryKey: ['fw-zones'], queryFn: listZones })
|
||||||
@@ -160,6 +162,7 @@ export default function ClientsTab() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -214,9 +217,11 @@ export default function ClientsTab() {
|
|||||||
dataSource={clients ?? []}
|
dataSource={clients ?? []}
|
||||||
columns={cols}
|
columns={cols}
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('wg.iface.addClient')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('wg.iface.addClient')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
emptyContent={
|
emptyContent={
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -224,9 +229,11 @@ export default function ClientsTab() {
|
|||||||
title={t('wg.iface.emptyClientTitle')}
|
title={t('wg.iface.emptyClientTitle')}
|
||||||
description={t('wg.iface.emptyClientDesc')}
|
description={t('wg.iface.emptyClientDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('wg.iface.addClient')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('wg.iface.addClient')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Alert, Button, Card, Col, Drawer, Form, Input, InputNumber,
|
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'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +12,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import apiClient, { isEnvelope } from '../../api/client'
|
import apiClient, { isEnvelope } from '../../api/client'
|
||||||
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import DataTable from '../../components/DataTable'
|
import DataTable from '../../components/DataTable'
|
||||||
import EmptyState from '../../components/EmptyState'
|
import EmptyState from '../../components/EmptyState'
|
||||||
import ActionButtons from '../../components/ActionButtons'
|
import ActionButtons from '../../components/ActionButtons'
|
||||||
@@ -98,6 +99,7 @@ async function listZones(): Promise<FwZoneLite[]> {
|
|||||||
export default function ServersTab() {
|
export default function ServersTab() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
|
|
||||||
const { data: servers, isLoading } = useQuery({ queryKey: ['wg', 'servers'], queryFn: listServers })
|
const { data: servers, isLoading } = useQuery({ queryKey: ['wg', 'servers'], queryFn: listServers })
|
||||||
const { data: zones } = useQuery({ queryKey: ['fw-zones'], queryFn: listZones })
|
const { data: zones } = useQuery({ queryKey: ['fw-zones'], queryFn: listZones })
|
||||||
@@ -152,6 +154,7 @@ export default function ServersTab() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
loading={quickToggle.isPending && quickToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => quickToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -208,9 +211,11 @@ export default function ServersTab() {
|
|||||||
dataSource={servers ?? []}
|
dataSource={servers ?? []}
|
||||||
columns={cols}
|
columns={cols}
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('wg.iface.addServer')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('wg.iface.addServer')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
emptyContent={
|
emptyContent={
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -218,9 +223,11 @@ export default function ServersTab() {
|
|||||||
title={t('wg.iface.emptyServerTitle')}
|
title={t('wg.iface.emptyServerTitle')}
|
||||||
description={t('wg.iface.emptyServerDesc')}
|
description={t('wg.iface.emptyServerDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('wg.iface.addServer')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openCreate}>
|
||||||
</Button>
|
{t('wg.iface.addServer')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -333,6 +340,7 @@ interface PeerDrawerProps {
|
|||||||
function PeerDrawer({ iface, onClose }: PeerDrawerProps) {
|
function PeerDrawer({ iface, onClose }: PeerDrawerProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
|
const isViewer = useAuthStore((s) => s.user?.role) === 'viewer'
|
||||||
const open = iface !== null
|
const open = iface !== null
|
||||||
const ifaceID = iface?.id ?? 0
|
const ifaceID = iface?.id ?? 0
|
||||||
|
|
||||||
@@ -432,6 +440,7 @@ function PeerDrawer({ iface, onClose }: PeerDrawerProps) {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={v}
|
checked={v}
|
||||||
|
disabled={isViewer}
|
||||||
loading={peerToggle.isPending && peerToggle.variables?.id === row.id}
|
loading={peerToggle.isPending && peerToggle.variables?.id === row.id}
|
||||||
onChange={(checked) => peerToggle.mutate({ id: row.id, row, checked })}
|
onChange={(checked) => peerToggle.mutate({ id: row.id, row, checked })}
|
||||||
/>
|
/>
|
||||||
@@ -504,9 +513,11 @@ function PeerDrawer({ iface, onClose }: PeerDrawerProps) {
|
|||||||
dataSource={peers ?? []}
|
dataSource={peers ?? []}
|
||||||
columns={cols}
|
columns={cols}
|
||||||
extraActions={
|
extraActions={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openPeerCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('wg.peer.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openPeerCreate}>
|
||||||
</Button>
|
{t('wg.peer.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
emptyContent={
|
emptyContent={
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -514,9 +525,11 @@ function PeerDrawer({ iface, onClose }: PeerDrawerProps) {
|
|||||||
title={t('wg.peer.emptyTitle')}
|
title={t('wg.peer.emptyTitle')}
|
||||||
description={t('wg.peer.emptyDesc')}
|
description={t('wg.peer.emptyDesc')}
|
||||||
action={
|
action={
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openPeerCreate}>
|
<Tooltip title={isViewer ? t('auth.viewerBadge') : undefined}>
|
||||||
{t('wg.peer.add')}
|
<Button type="primary" icon={<PlusOutlined />} disabled={isViewer} onClick={openPeerCreate}>
|
||||||
</Button>
|
{t('wg.peer.add')}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user