rights system
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<AppRole> {
|
||||
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';
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,12 @@ import logger from '../utils/logger';
|
||||
|
||||
// Default configs — used when no DB config exists yet
|
||||
const DEFAULT_GROUP_HIERARCHY: Record<string, string[]> = {
|
||||
'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': [],
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user