feat(rbac): Viewer-Rolle durchsetzen — alle Mutations nur für Admins
- middleware.go: RequireAdminForMutations() blockiert POST/PUT/DELETE für Nicht-Admins (GET/HEAD passieren immer durch) - main.go: Middleware in den authed-Gruppe eingehängt — wirkt für alle ~30 Resource-Handler gleichzeitig - api/client.ts: 403 → AntD-Notification "Nur Lesezugriff" statt stiller Fehler - Header: "Nur lesen"-Badge + Tooltip wenn role=viewer - i18n de+en: viewerBadge + viewerHint Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import axios, { type AxiosError } from 'axios'
|
||||
import { message } from 'antd'
|
||||
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
@@ -38,6 +39,12 @@ apiClient.interceptors.response.use(
|
||||
window.location.replace('/login')
|
||||
}
|
||||
|
||||
// 403 admin_required → viewer account tried to mutate; show a
|
||||
// one-time notification and let the calling mutation handle the rest.
|
||||
if (error.response?.status === 403) {
|
||||
message.error('Keine Berechtigung — dieser Account hat nur Lesezugriff.', 4)
|
||||
}
|
||||
|
||||
// 503 setup_required → kick to /setup so the wizard takes over.
|
||||
if (
|
||||
error.response?.status === 503 &&
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button, Dropdown, Select, Space } from 'antd'
|
||||
import { GlobalOutlined, LogoutOutlined, MenuOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import { Button, Dropdown, Select, Space, Tag, Tooltip } from 'antd'
|
||||
import { EyeOutlined, GlobalOutlined, LogoutOutlined, MenuOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -55,23 +55,32 @@ export default function Header({ pageTitle, onMenuToggle }: HeaderProps) {
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
{user && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
label: t('auth.logout'),
|
||||
onClick: onLogout,
|
||||
},
|
||||
],
|
||||
}}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button type="text" className="header-user">
|
||||
<Space><UserOutlined />{user.actor}</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
<>
|
||||
{user.role === 'viewer' && (
|
||||
<Tooltip title={t('auth.viewerHint')}>
|
||||
<Tag icon={<EyeOutlined />} color="default" style={{ marginRight: 4 }}>
|
||||
{t('auth.viewerBadge')}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
key: 'logout',
|
||||
icon: <LogoutOutlined />,
|
||||
label: t('auth.logout'),
|
||||
onClick: onLogout,
|
||||
},
|
||||
],
|
||||
}}
|
||||
placement="bottomRight"
|
||||
>
|
||||
<Button type="text" className="header-user">
|
||||
<Space><UserOutlined />{user.actor}</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -198,7 +198,9 @@
|
||||
"logout": "Abmelden",
|
||||
"loginFailed": "Anmeldung fehlgeschlagen",
|
||||
"loggedInAs": "Angemeldet als",
|
||||
"forgotPassword": "Passwort vergessen?"
|
||||
"forgotPassword": "Passwort vergessen?",
|
||||
"viewerBadge": "Nur lesen",
|
||||
"viewerHint": "Dieser Account hat die Rolle Betrachter — Änderungen sind gesperrt. Ein Admin kann die Rolle anpassen."
|
||||
},
|
||||
"reset": {
|
||||
"title": "Admin-Passwort zurücksetzen",
|
||||
|
||||
@@ -198,7 +198,9 @@
|
||||
"logout": "Sign out",
|
||||
"loginFailed": "Sign-in failed",
|
||||
"loggedInAs": "Signed in as",
|
||||
"forgotPassword": "Forgot your password?"
|
||||
"forgotPassword": "Forgot your password?",
|
||||
"viewerBadge": "Read-only",
|
||||
"viewerHint": "Your account has viewer role — all changes are blocked. Contact an admin to change your role."
|
||||
},
|
||||
"reset": {
|
||||
"title": "Reset admin password",
|
||||
|
||||
Reference in New Issue
Block a user