From fa10467f21eb420eed6dd001df9ed2c71204d3f2 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Mon, 23 Mar 2026 12:12:21 +0100 Subject: [PATCH] rights system --- .../037_create_permission_system.sql | 47 ------------------- backend/src/middleware/rbac.middleware.ts | 3 +- backend/src/services/permission.service.ts | 5 +- .../admin/NotificationBroadcastTab.tsx | 15 +++--- .../src/components/admin/UserOverviewTab.tsx | 4 +- .../dashboard/AdminStatusWidget.tsx | 10 ++-- frontend/src/pages/AdminSettings.tsx | 10 ++-- frontend/src/pages/UebungDetail.tsx | 28 +++-------- 8 files changed, 32 insertions(+), 90 deletions(-) diff --git a/backend/src/database/migrations/037_create_permission_system.sql b/backend/src/database/migrations/037_create_permission_system.sql index 989a5de..4d16155 100644 --- a/backend/src/database/migrations/037_create_permission_system.sql +++ b/backend/src/database/migrations/037_create_permission_system.sql @@ -212,53 +212,6 @@ INSERT INTO group_permissions (authentik_group, permission_id) VALUES ('dashboard_kommando', 'admin:view') ON CONFLICT DO NOTHING; --- ── dashboard_gruppenfuehrer — write level for most ── -INSERT INTO group_permissions (authentik_group, permission_id) VALUES - -- Kalender - ('dashboard_gruppenfuehrer', 'kalender:view'), - ('dashboard_gruppenfuehrer', 'kalender:create'), - ('dashboard_gruppenfuehrer', 'kalender:mark_attendance'), - ('dashboard_gruppenfuehrer', 'kalender:create_bookings'), - ('dashboard_gruppenfuehrer', 'kalender:edit_bookings'), - ('dashboard_gruppenfuehrer', 'kalender:cancel_own_bookings'), - ('dashboard_gruppenfuehrer', 'kalender:manage_categories'), - ('dashboard_gruppenfuehrer', 'kalender:widget_events'), - ('dashboard_gruppenfuehrer', 'kalender:widget_bookings'), - ('dashboard_gruppenfuehrer', 'kalender:widget_quick_add'), - -- Fahrzeuge - ('dashboard_gruppenfuehrer', 'fahrzeuge:view'), - ('dashboard_gruppenfuehrer', 'fahrzeuge:change_status'), - ('dashboard_gruppenfuehrer', 'fahrzeuge:manage_maintenance'), - ('dashboard_gruppenfuehrer', 'fahrzeuge:widget'), - -- Einsätze - ('dashboard_gruppenfuehrer', 'einsaetze:view'), - ('dashboard_gruppenfuehrer', 'einsaetze:create'), - ('dashboard_gruppenfuehrer', 'einsaetze:manage_personnel'), - -- Ausrüstung - ('dashboard_gruppenfuehrer', 'ausruestung:view'), - ('dashboard_gruppenfuehrer', 'ausruestung:create'), - ('dashboard_gruppenfuehrer', 'ausruestung:manage_maintenance'), - ('dashboard_gruppenfuehrer', 'ausruestung:widget'), - -- Mitglieder - ('dashboard_gruppenfuehrer', 'mitglieder:view_own'), - ('dashboard_gruppenfuehrer', 'mitglieder:view_all'), - -- Atemschutz - ('dashboard_gruppenfuehrer', 'atemschutz:view'), - ('dashboard_gruppenfuehrer', 'atemschutz:create'), - ('dashboard_gruppenfuehrer', 'atemschutz:widget'), - -- Wissen - ('dashboard_gruppenfuehrer', 'wissen:view'), - ('dashboard_gruppenfuehrer', 'wissen:widget_recent'), - ('dashboard_gruppenfuehrer', 'wissen:widget_search'), - -- Vikunja - ('dashboard_gruppenfuehrer', 'vikunja:create_tasks'), - ('dashboard_gruppenfuehrer', 'vikunja:widget_tasks'), - ('dashboard_gruppenfuehrer', 'vikunja:widget_quick_add'), - -- Dashboard - ('dashboard_gruppenfuehrer', 'dashboard:widget_links'), - ('dashboard_gruppenfuehrer', 'dashboard:widget_banner') -ON CONFLICT DO NOTHING; - -- ── dashboard_fahrmeister — vehicle specialist ── INSERT INTO group_permissions (authentik_group, permission_id) VALUES -- Kalender diff --git a/backend/src/middleware/rbac.middleware.ts b/backend/src/middleware/rbac.middleware.ts index aea7f71..451083f 100644 --- a/backend/src/middleware/rbac.middleware.ts +++ b/backend/src/middleware/rbac.middleware.ts @@ -96,7 +96,6 @@ export function resolveRequestRole(req: Request): AppRole { if (groups.includes('dashboard_admin')) return 'admin'; if (groups.includes('dashboard_kommando')) return 'kommandant'; if ( - groups.includes('dashboard_gruppenfuehrer') || groups.includes('dashboard_fahrmeister') || groups.includes('dashboard_zeugmeister') || groups.includes('dashboard_chargen') @@ -112,7 +111,7 @@ export function getUserRole(_userId: string): Promise { export function roleFromGroups(groups: string[]): AppRole { if (groups.includes('dashboard_admin')) return 'admin'; if (groups.includes('dashboard_kommando')) return 'kommandant'; - if (groups.includes('dashboard_gruppenfuehrer') || groups.includes('dashboard_fahrmeister') || groups.includes('dashboard_zeugmeister') || groups.includes('dashboard_chargen')) return 'gruppenfuehrer'; + if (groups.includes('dashboard_fahrmeister') || groups.includes('dashboard_zeugmeister') || groups.includes('dashboard_chargen')) return 'gruppenfuehrer'; return 'mitglied'; } diff --git a/backend/src/services/permission.service.ts b/backend/src/services/permission.service.ts index ce68c50..4398262 100644 --- a/backend/src/services/permission.service.ts +++ b/backend/src/services/permission.service.ts @@ -3,13 +3,12 @@ import logger from '../utils/logger'; // Default configs — used when no DB config exists yet const DEFAULT_GROUP_HIERARCHY: Record = { - 'dashboard_mitglied': ['dashboard_chargen', 'dashboard_atemschutz', 'dashboard_moderator', 'dashboard_zeugmeister', 'dashboard_fahrmeister', 'dashboard_gruppenfuehrer', 'dashboard_kommando'], - 'dashboard_chargen': ['dashboard_gruppenfuehrer', 'dashboard_kommando'], + 'dashboard_mitglied': ['dashboard_chargen', 'dashboard_atemschutz', 'dashboard_moderator', 'dashboard_zeugmeister', 'dashboard_fahrmeister', 'dashboard_kommando'], + 'dashboard_chargen': ['dashboard_kommando'], 'dashboard_atemschutz': ['dashboard_kommando'], 'dashboard_moderator': ['dashboard_kommando'], 'dashboard_zeugmeister': ['dashboard_kommando'], 'dashboard_fahrmeister': ['dashboard_kommando'], - 'dashboard_gruppenfuehrer': ['dashboard_kommando'], 'dashboard_kommando': [], }; diff --git a/frontend/src/components/admin/NotificationBroadcastTab.tsx b/frontend/src/components/admin/NotificationBroadcastTab.tsx index e11e63a..4dd2ab8 100644 --- a/frontend/src/components/admin/NotificationBroadcastTab.tsx +++ b/frontend/src/components/admin/NotificationBroadcastTab.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef, useCallback } from 'react'; +import { useQuery } from '@tanstack/react-query'; import { Box, TextField, @@ -36,14 +37,16 @@ const DIENSTGRAD_OPTIONS = [ 'Kommandant', ]; -const GROUP_OPTIONS = [ - 'dashboard_admin', - 'dashboard_kommando', - 'dashboard_gruppenfuehrer', -]; function NotificationBroadcastTab() { const { showSuccess, showError } = useNotification(); + const { data: groupOptions = [] } = useQuery({ + queryKey: ['admin-permission-groups'], + queryFn: async () => { + const { permissionsApi } = await import('../../services/permissions'); + return permissionsApi.getGroups(); + }, + }); const [titel, setTitel] = useState(''); const [nachricht, setNachricht] = useState(''); const [schwere, setSchwere] = useState<'info' | 'warnung' | 'fehler'>('info'); @@ -198,7 +201,7 @@ function NotificationBroadcastTab() { Keine Einschraenkung - {GROUP_OPTIONS.map((g) => ( + {groupOptions.map((g) => ( {g} ))} diff --git a/frontend/src/components/admin/UserOverviewTab.tsx b/frontend/src/components/admin/UserOverviewTab.tsx index 0cb5eb3..a3bf74d 100644 --- a/frontend/src/components/admin/UserOverviewTab.tsx +++ b/frontend/src/components/admin/UserOverviewTab.tsx @@ -22,8 +22,10 @@ function getRoleFromGroups(groups: string[] | null): string { if (!groups) return 'Mitglied'; if (groups.includes('dashboard_admin')) return 'Admin'; if (groups.includes('dashboard_kommando')) return 'Kommandant'; - if (groups.includes('dashboard_gruppenfuehrer')) return 'Gruppenführer'; if (groups.includes('dashboard_moderator')) return 'Moderator'; + if (groups.includes('dashboard_fahrmeister')) return 'Fahrmeister'; + if (groups.includes('dashboard_zeugmeister')) return 'Zeugmeister'; + if (groups.includes('dashboard_chargen')) return 'Chargen'; if (groups.includes('dashboard_atemschutz')) return 'Atemschutz'; return 'Mitglied'; } diff --git a/frontend/src/components/dashboard/AdminStatusWidget.tsx b/frontend/src/components/dashboard/AdminStatusWidget.tsx index 3ccc86c..1858de2 100644 --- a/frontend/src/components/dashboard/AdminStatusWidget.tsx +++ b/frontend/src/components/dashboard/AdminStatusWidget.tsx @@ -4,25 +4,25 @@ import { useNavigate } from 'react-router-dom'; import { MonitorHeartOutlined } from '@mui/icons-material'; import { adminApi } from '../../services/admin'; import { useCountUp } from '../../hooks/useCountUp'; -import { useAuth } from '../../contexts/AuthContext'; +import { usePermissionContext } from '../../contexts/PermissionContext'; function AdminStatusWidget() { - const { user } = useAuth(); + const { hasPermission } = usePermissionContext(); const navigate = useNavigate(); - const isAdmin = user?.groups?.includes('dashboard_admin') ?? false; + const canView = hasPermission('admin:view'); const { data } = useQuery({ queryKey: ['admin-status-summary'], queryFn: () => adminApi.getStatusSummary(), refetchInterval: 30_000, - enabled: isAdmin, + enabled: canView, }); const up = useCountUp(data?.up ?? 0); const total = useCountUp(data?.total ?? 0); - if (!isAdmin) return null; + if (!canView) return null; const allUp = data && data.up === data.total; const majorityDown = data && data.total > 0 && data.up < data.total / 2; diff --git a/frontend/src/pages/AdminSettings.tsx b/frontend/src/pages/AdminSettings.tsx index cbf1503..0a30a85 100644 --- a/frontend/src/pages/AdminSettings.tsx +++ b/frontend/src/pages/AdminSettings.tsx @@ -31,7 +31,7 @@ import { import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Navigate } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; -import { useAuth } from '../contexts/AuthContext'; +import { usePermissionContext } from '../contexts/PermissionContext'; import { useNotification } from '../contexts/NotificationContext'; import { settingsApi } from '../services/settings'; @@ -61,11 +61,11 @@ const ADMIN_INTERVAL_OPTIONS = [ ]; function AdminSettings() { - const { user } = useAuth(); + const { hasPermission } = usePermissionContext(); const { showSuccess, showError } = useNotification(); const queryClient = useQueryClient(); - const isAdmin = user?.groups?.includes('dashboard_admin') ?? false; + const canAccess = hasPermission('admin:write'); // State for link collections const [linkCollections, setLinkCollections] = useState([]); @@ -86,7 +86,7 @@ function AdminSettings() { const { data: settings, isLoading } = useQuery({ queryKey: ['admin-settings'], queryFn: () => settingsApi.getAll(), - enabled: isAdmin, + enabled: canAccess, }); // Initialize state from fetched settings @@ -197,7 +197,7 @@ function AdminSettings() { reader.readAsDataURL(file); }; - if (!isAdmin) { + if (!canAccess) { return ; } diff --git a/frontend/src/pages/UebungDetail.tsx b/frontend/src/pages/UebungDetail.tsx index 060dcdc..2f8b148 100644 --- a/frontend/src/pages/UebungDetail.tsx +++ b/frontend/src/pages/UebungDetail.tsx @@ -42,7 +42,7 @@ import { } from '@mui/icons-material'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import DashboardLayout from '../components/dashboard/DashboardLayout'; -import { useAuth } from '../contexts/AuthContext'; +import { usePermissionContext } from '../contexts/PermissionContext'; import { trainingApi } from '../services/training'; import type { TeilnahmeStatus, UebungTyp, Teilnahme } from '../types/training.types'; @@ -75,18 +75,6 @@ function formatTime(iso: string): string { return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')} Uhr`; } -// --------------------------------------------------------------------------- -// Role helper — reads `role` from the user object (added by Tier 1) -// --------------------------------------------------------------------------- - -const ROLE_ORDER: Record = { - mitglied: 0, gruppenfuehrer: 1, kommandant: 2, admin: 3, -}; - -function hasRole(userRole: string | undefined, minRole: string): boolean { - return (ROLE_ORDER[userRole ?? 'mitglied'] ?? 0) >= (ROLE_ORDER[minRole] ?? 0); -} - // --------------------------------------------------------------------------- // RSVP Status icon // --------------------------------------------------------------------------- @@ -199,7 +187,7 @@ function MarkAttendanceDialog({ function AttendeeAccordion({ teilnahmen, counts, - userRole, + canSeeList, }: { teilnahmen?: Teilnahme[]; counts: { @@ -210,9 +198,8 @@ function AttendeeAccordion({ anzahl_erschienen: number; gesamt_eingeladen: number; }; - userRole?: string; + canSeeList: boolean; }) { - const canSeeList = hasRole(userRole, 'gruppenfuehrer'); return ( @@ -279,14 +266,13 @@ function AttendeeAccordion({ export default function UebungDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); - const { user } = useAuth(); + const { hasPermission } = usePermissionContext(); const queryClient = useQueryClient(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); - // We cast user to include `role` (added by Tier 1) - const userRole = (user as any)?.role as string | undefined; - const canWrite = hasRole(userRole, 'gruppenfuehrer'); + const canWrite = hasPermission('kalender:create'); + const canSeeAttendees = hasPermission('kalender:mark_attendance'); const [markAttendanceOpen, setMarkAttendanceOpen] = useState(false); const [rsvpLoading, setRsvpLoading] = useState<'zugesagt' | 'abgesagt' | null>(null); @@ -531,7 +517,7 @@ export default function UebungDetail() {