rights system

This commit is contained in:
Matthias Hochmeister
2026-03-23 12:12:21 +01:00
parent a575b61d26
commit fa10467f21
8 changed files with 32 additions and 90 deletions

View File

@@ -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<string[]>({
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() {
<MenuItem value="">
<em>Keine Einschraenkung</em>
</MenuItem>
{GROUP_OPTIONS.map((g) => (
{groupOptions.map((g) => (
<MenuItem key={g} value={g}>{g}</MenuItem>
))}
</TextField>

View File

@@ -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';
}

View File

@@ -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;

View File

@@ -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<LinkCollection[]>([]);
@@ -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 <Navigate to="/dashboard" replace />;
}

View File

@@ -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<string, number> = {
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 (
<Accordion disableGutters elevation={0} sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
@@ -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() {
<AttendeeAccordion
teilnahmen={event.teilnahmen}
counts={event}
userRole={userRole}
canSeeList={canSeeAttendees}
/>
</Paper>
</Box>