rights system

This commit is contained in:
Matthias Hochmeister
2026-03-23 10:07:53 +01:00
parent f976f36cbc
commit 2bb22850f4
35 changed files with 1565 additions and 282 deletions

View File

@@ -9,6 +9,7 @@ import NotificationBroadcastTab from '../components/admin/NotificationBroadcastT
import BannerManagementTab from '../components/admin/BannerManagementTab';
import ServiceModeTab from '../components/admin/ServiceModeTab';
import FdiskSyncTab from '../components/admin/FdiskSyncTab';
import PermissionMatrixTab from '../components/admin/PermissionMatrixTab';
import { useAuth } from '../contexts/AuthContext';
interface TabPanelProps {
@@ -22,7 +23,7 @@ function TabPanel({ children, value, index }: TabPanelProps) {
return <Box sx={{ pt: 3 }}>{children}</Box>;
}
const ADMIN_TAB_COUNT = 7;
const ADMIN_TAB_COUNT = 8;
function AdminDashboard() {
const navigate = useNavigate();
@@ -57,6 +58,7 @@ function AdminDashboard() {
<Tab label="Banner" />
<Tab label="Wartung" />
<Tab label="FDISK Sync" />
<Tab label="Berechtigungen" />
</Tabs>
</Box>
@@ -81,6 +83,9 @@ function AdminDashboard() {
<TabPanel value={tab} index={6}>
<FdiskSyncTab />
</TabPanel>
<TabPanel value={tab} index={7}>
<PermissionMatrixTab />
</TabPanel>
</DashboardLayout>
);
}

View File

@@ -45,7 +45,7 @@ import ChatAwareFab from '../components/shared/ChatAwareFab';
import { atemschutzApi } from '../services/atemschutz';
import { membersService } from '../services/members';
import { useNotification } from '../contexts/NotificationContext';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { toGermanDate, fromGermanDate, isValidGermanDate } from '../utils/dateInput';
import type {
AtemschutzUebersicht,
@@ -142,10 +142,9 @@ const StatCard: React.FC<StatCardProps> = ({ label, value, color, bgcolor }) =>
function Atemschutz() {
const notification = useNotification();
const { user } = useAuth();
const ATEMSCHUTZ_PRIVILEGED = ['dashboard_admin', 'dashboard_kommando', 'dashboard_atemschutz', 'dashboard_moderator'];
const canViewAll = user?.groups?.some(g => ATEMSCHUTZ_PRIVILEGED.includes(g)) ?? false;
const canWrite = canViewAll;
const { hasPermission } = usePermissionContext();
const canViewAll = hasPermission('atemschutz:view');
const canWrite = hasPermission('atemschutz:create');
// Data state
const [traeger, setTraeger] = useState<AtemschutzUebersicht[]>([]);

View File

@@ -6,6 +6,7 @@ import {
} from '@mui/material';
import { useQuery } from '@tanstack/react-query';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import SkeletonCard from '../components/shared/SkeletonCard';
import UserProfile from '../components/dashboard/UserProfile';
@@ -33,13 +34,7 @@ import { WidgetKey } from '../constants/widgets';
function Dashboard() {
const { user } = useAuth();
const isAdmin = user?.groups?.includes('dashboard_admin') ?? false;
const canViewAtemschutz = user?.groups?.some(g =>
['dashboard_admin', 'dashboard_kommando', 'dashboard_atemschutz', 'dashboard_moderator'].includes(g)
) ?? false;
const canWrite = user?.groups?.some(g =>
['dashboard_admin', 'dashboard_kommando', 'dashboard_moderator', 'dashboard_gruppenfuehrer'].includes(g)
) ?? false;
const { hasPermission, isAdmin } = usePermissionContext();
const [dataLoading, setDataLoading] = useState(true);
const { data: preferences } = useQuery({
@@ -120,7 +115,7 @@ function Dashboard() {
</Fade>
)}
{canViewAtemschutz && widgetVisible('atemschutz') && (
{hasPermission('atemschutz:widget') && widgetVisible('atemschutz') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '400ms' }}>
<Box>
<AtemschutzDashboardCard />
@@ -163,7 +158,7 @@ function Dashboard() {
</Fade>
)}
{canWrite && widgetVisible('eventQuickAdd') && (
{hasPermission('kalender:widget_quick_add') && widgetVisible('eventQuickAdd') && (
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '600ms' }}>
<Box>
<EventQuickAddWidget />

View File

@@ -49,7 +49,7 @@ import {
EINSATZ_STATUS_LABELS,
} from '../services/incidents';
import CreateEinsatzDialog from '../components/incidents/CreateEinsatzDialog';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
// ---------------------------------------------------------------------------
// COLOUR MAP for Einsatzart chips
@@ -176,10 +176,8 @@ function StatsSummaryBar({ stats, loading }: StatsSummaryProps) {
// ---------------------------------------------------------------------------
function Einsaetze() {
const navigate = useNavigate();
const { user } = useAuth();
const canWrite = user?.groups?.some((g: string) =>
['dashboard_admin', 'dashboard_kommando', 'dashboard_gruppenfuehrer'].includes(g)
) ?? false;
const { hasPermission } = usePermissionContext();
const canWrite = hasPermission('einsaetze:create');
// List state
const [items, setItems] = useState<EinsatzListItem[]>([]);

View File

@@ -43,7 +43,7 @@ import {
EinsatzArt,
} from '../services/incidents';
import { useNotification } from '../contexts/NotificationContext';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
// ---------------------------------------------------------------------------
// COLOUR MAPS
@@ -165,10 +165,8 @@ function EinsatzDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const notification = useNotification();
const { user } = useAuth();
const canWrite = user?.groups?.some((g: string) =>
['dashboard_admin', 'dashboard_kommando', 'dashboard_gruppenfuehrer'].includes(g)
) ?? false;
const { hasPermission } = usePermissionContext();
const canWrite = hasPermission('einsaetze:create');
const [einsatz, setEinsatz] = useState<EinsatzDetailType | null>(null);
const [loading, setLoading] = useState(true);

View File

@@ -46,6 +46,7 @@ import {
import DashboardLayout from '../components/dashboard/DashboardLayout';
import ChatAwareFab from '../components/shared/ChatAwareFab';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext';
import { bookingApi, fetchVehicles } from '../services/bookings';
import type {
@@ -85,21 +86,17 @@ const EMPTY_FORM: CreateBuchungInput = {
kontaktTelefon: '',
};
const WRITE_GROUPS = ['dashboard_admin', 'dashboard_fahrmeister', 'dashboard_moderator'];
const MANAGE_ART_GROUPS = ['dashboard_admin', 'dashboard_moderator'];
// ---------------------------------------------------------------------------
// Main Page
// ---------------------------------------------------------------------------
function FahrzeugBuchungen() {
const { user } = useAuth();
const { hasPermission } = usePermissionContext();
const notification = useNotification();
const canCreate = !!user; // All authenticated users can create bookings
const canWrite =
user?.groups?.some((g) => WRITE_GROUPS.includes(g)) ?? false; // Can edit/cancel
const canChangeBuchungsArt =
user?.groups?.some((g) => MANAGE_ART_GROUPS.includes(g)) ?? false; // Can change booking type
const canCreate = hasPermission('kalender:create_bookings');
const canWrite = hasPermission('kalender:edit_bookings');
const canChangeBuchungsArt = hasPermission('kalender:manage_categories');
// ── Week navigation ────────────────────────────────────────────────────────
const [currentWeekStart, setCurrentWeekStart] = useState<Date>(() =>

View File

@@ -72,6 +72,7 @@ import DashboardLayout from '../components/dashboard/DashboardLayout';
import ChatAwareFab from '../components/shared/ChatAwareFab';
import { toGermanDateTime, fromGermanDate, fromGermanDateTime, isValidGermanDateTime } from '../utils/dateInput';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext';
import { trainingApi } from '../services/training';
import { eventsApi } from '../services/events';
@@ -117,9 +118,6 @@ import { de } from 'date-fns/locale';
// Constants
// ──────────────────────────────────────────────────────────────────────────────
const WRITE_GROUPS_EVENTS = ['dashboard_admin', 'dashboard_moderator'];
const WRITE_GROUPS_BOOKINGS = ['dashboard_admin', 'dashboard_fahrmeister', 'dashboard_moderator'];
const WEEKDAY_LABELS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
const MONTH_LABELS = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
@@ -1704,15 +1702,14 @@ export default function Kalender() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const { user } = useAuth();
const { hasPermission } = usePermissionContext();
const notification = useNotification();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const canWriteEvents =
user?.groups?.some((g) => WRITE_GROUPS_EVENTS.includes(g)) ?? false;
const canWriteBookings =
user?.groups?.some((g) => WRITE_GROUPS_BOOKINGS.includes(g)) ?? false;
const canCreateBookings = !!user;
const canWriteEvents = hasPermission('kalender:create_events');
const canWriteBookings = hasPermission('kalender:edit_bookings');
const canCreateBookings = hasPermission('kalender:create_bookings');
// ── Tab ─────────────────────────────────────────────────────────────────────
const [activeTab, setActiveTab] = useState(() => {

View File

@@ -40,6 +40,7 @@ import {
import { useParams, useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { membersService } from '../services/members';
import { atemschutzApi } from '../services/atemschutz';
import { toGermanDate, fromGermanDate } from '../utils/dateInput';
@@ -67,9 +68,8 @@ import { UntersuchungErgebnisLabel } from '../types/atemschutz.types';
// Role helpers
// ----------------------------------------------------------------
function useCanWrite(): boolean {
const { user } = useAuth();
const groups: string[] = (user as any)?.groups ?? [];
return groups.includes('dashboard_admin') || groups.includes('dashboard_kommando');
const { hasPermission } = usePermissionContext();
return hasPermission('mitglieder:edit');
}
function useCurrentUserId(): string | undefined {

View File

@@ -34,6 +34,7 @@ import { useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import ChatAwareFab from '../components/shared/ChatAwareFab';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { membersService } from '../services/members';
import {
MemberListItem,
@@ -51,9 +52,8 @@ import {
// Helper: determine whether the current user can write member data
// ----------------------------------------------------------------
function useCanWrite(): boolean {
const { user } = useAuth();
const groups: string[] = (user as any)?.groups ?? [];
return groups.includes('dashboard_admin') || groups.includes('dashboard_kommando');
const { hasPermission } = usePermissionContext();
return hasPermission('mitglieder:edit');
}
// ----------------------------------------------------------------
@@ -73,17 +73,17 @@ function useDebounce<T>(value: T, delay: number): T {
// ----------------------------------------------------------------
function Mitglieder() {
const navigate = useNavigate();
const { user } = useAuth(); const canWrite = useCanWrite();
const { user } = useAuth();
const canWrite = useCanWrite();
const { hasPermission } = usePermissionContext();
// --- redirect non-admin/non-kommando users to their own profile ---
// --- redirect non-privileged users to their own profile ---
useEffect(() => {
if (!user) return;
const groups: string[] = (user as any)?.groups ?? [];
const isAdmin = groups.includes('dashboard_admin') || groups.includes('dashboard_kommando');
if (!isAdmin) {
if (!hasPermission('mitglieder:edit')) {
navigate(`/mitglieder/${(user as any).id}`, { replace: true });
}
}, [user, navigate]);
}, [user, navigate, hasPermission]);
// --- data state ---
const [members, setMembers] = useState<MemberListItem[]>([]);

View File

@@ -34,7 +34,7 @@ import {
Category as CategoryIcon,
} from '@mui/icons-material';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext';
import { eventsApi } from '../services/events';
import type { VeranstaltungKategorie, GroupInfo } from '../types/events.types';
@@ -298,10 +298,9 @@ function DeleteDialog({ open, kategorie, onClose, onDeleted }: DeleteDialogProps
// ---------------------------------------------------------------------------
export default function VeranstaltungKategorien() {
const { user } = useAuth();
const { hasPermission } = usePermissionContext();
const canManage =
user?.groups?.some((g) => ['dashboard_admin', 'dashboard_moderator'].includes(g)) ?? false;
const canManage = hasPermission('kalender:manage_categories');
const [kategorien, setKategorien] = useState<VeranstaltungKategorie[]>([]);
const [groups, setGroups] = useState<GroupInfo[]>([]);

View File

@@ -52,7 +52,7 @@ import {
import DashboardLayout from '../components/dashboard/DashboardLayout';
import ChatAwareFab from '../components/shared/ChatAwareFab';
import { toGermanDate, toGermanDateTime, fromGermanDate, fromGermanDateTime } from '../utils/dateInput';
import { useAuth } from '../contexts/AuthContext';
import { usePermissionContext } from '../contexts/PermissionContext';
import { useNotification } from '../contexts/NotificationContext';
import { eventsApi } from '../services/events';
import type {
@@ -1069,13 +1069,12 @@ function EventListView({ events, canWrite, onEdit, onCancel, onDelete }: ListVie
// ---------------------------------------------------------------------------
export default function Veranstaltungen() {
const { user } = useAuth();
const { hasPermission } = usePermissionContext();
const notification = useNotification();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const canWrite =
user?.groups?.some((g) => ['dashboard_admin', 'dashboard_moderator'].includes(g)) ?? false;
const canWrite = hasPermission('kalender:create_events');
const today = new Date();
const [viewMonth, setViewMonth] = useState({ year: today.getFullYear(), month: today.getMonth() });