rights system
This commit is contained in:
@@ -212,53 +212,6 @@ INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
|||||||
('dashboard_kommando', 'admin:view')
|
('dashboard_kommando', 'admin:view')
|
||||||
ON CONFLICT DO NOTHING;
|
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 ──
|
-- ── dashboard_fahrmeister — vehicle specialist ──
|
||||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||||
-- Kalender
|
-- Kalender
|
||||||
|
|||||||
@@ -96,7 +96,6 @@ export function resolveRequestRole(req: Request): AppRole {
|
|||||||
if (groups.includes('dashboard_admin')) return 'admin';
|
if (groups.includes('dashboard_admin')) return 'admin';
|
||||||
if (groups.includes('dashboard_kommando')) return 'kommandant';
|
if (groups.includes('dashboard_kommando')) return 'kommandant';
|
||||||
if (
|
if (
|
||||||
groups.includes('dashboard_gruppenfuehrer') ||
|
|
||||||
groups.includes('dashboard_fahrmeister') ||
|
groups.includes('dashboard_fahrmeister') ||
|
||||||
groups.includes('dashboard_zeugmeister') ||
|
groups.includes('dashboard_zeugmeister') ||
|
||||||
groups.includes('dashboard_chargen')
|
groups.includes('dashboard_chargen')
|
||||||
@@ -112,7 +111,7 @@ export function getUserRole(_userId: string): Promise<AppRole> {
|
|||||||
export function roleFromGroups(groups: string[]): AppRole {
|
export function roleFromGroups(groups: string[]): AppRole {
|
||||||
if (groups.includes('dashboard_admin')) return 'admin';
|
if (groups.includes('dashboard_admin')) return 'admin';
|
||||||
if (groups.includes('dashboard_kommando')) return 'kommandant';
|
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';
|
return 'mitglied';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import logger from '../utils/logger';
|
|||||||
|
|
||||||
// Default configs — used when no DB config exists yet
|
// Default configs — used when no DB config exists yet
|
||||||
const DEFAULT_GROUP_HIERARCHY: Record<string, string[]> = {
|
const DEFAULT_GROUP_HIERARCHY: Record<string, string[]> = {
|
||||||
'dashboard_mitglied': ['dashboard_chargen', 'dashboard_atemschutz', 'dashboard_moderator', 'dashboard_zeugmeister', 'dashboard_fahrmeister', 'dashboard_gruppenfuehrer', 'dashboard_kommando'],
|
'dashboard_mitglied': ['dashboard_chargen', 'dashboard_atemschutz', 'dashboard_moderator', 'dashboard_zeugmeister', 'dashboard_fahrmeister', 'dashboard_kommando'],
|
||||||
'dashboard_chargen': ['dashboard_gruppenfuehrer', 'dashboard_kommando'],
|
'dashboard_chargen': ['dashboard_kommando'],
|
||||||
'dashboard_atemschutz': ['dashboard_kommando'],
|
'dashboard_atemschutz': ['dashboard_kommando'],
|
||||||
'dashboard_moderator': ['dashboard_kommando'],
|
'dashboard_moderator': ['dashboard_kommando'],
|
||||||
'dashboard_zeugmeister': ['dashboard_kommando'],
|
'dashboard_zeugmeister': ['dashboard_kommando'],
|
||||||
'dashboard_fahrmeister': ['dashboard_kommando'],
|
'dashboard_fahrmeister': ['dashboard_kommando'],
|
||||||
'dashboard_gruppenfuehrer': ['dashboard_kommando'],
|
|
||||||
'dashboard_kommando': [],
|
'dashboard_kommando': [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
TextField,
|
TextField,
|
||||||
@@ -36,14 +37,16 @@ const DIENSTGRAD_OPTIONS = [
|
|||||||
'Kommandant',
|
'Kommandant',
|
||||||
];
|
];
|
||||||
|
|
||||||
const GROUP_OPTIONS = [
|
|
||||||
'dashboard_admin',
|
|
||||||
'dashboard_kommando',
|
|
||||||
'dashboard_gruppenfuehrer',
|
|
||||||
];
|
|
||||||
|
|
||||||
function NotificationBroadcastTab() {
|
function NotificationBroadcastTab() {
|
||||||
const { showSuccess, showError } = useNotification();
|
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 [titel, setTitel] = useState('');
|
||||||
const [nachricht, setNachricht] = useState('');
|
const [nachricht, setNachricht] = useState('');
|
||||||
const [schwere, setSchwere] = useState<'info' | 'warnung' | 'fehler'>('info');
|
const [schwere, setSchwere] = useState<'info' | 'warnung' | 'fehler'>('info');
|
||||||
@@ -198,7 +201,7 @@ function NotificationBroadcastTab() {
|
|||||||
<MenuItem value="">
|
<MenuItem value="">
|
||||||
<em>Keine Einschraenkung</em>
|
<em>Keine Einschraenkung</em>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{GROUP_OPTIONS.map((g) => (
|
{groupOptions.map((g) => (
|
||||||
<MenuItem key={g} value={g}>{g}</MenuItem>
|
<MenuItem key={g} value={g}>{g}</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ function getRoleFromGroups(groups: string[] | null): string {
|
|||||||
if (!groups) return 'Mitglied';
|
if (!groups) return 'Mitglied';
|
||||||
if (groups.includes('dashboard_admin')) return 'Admin';
|
if (groups.includes('dashboard_admin')) return 'Admin';
|
||||||
if (groups.includes('dashboard_kommando')) return 'Kommandant';
|
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_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';
|
if (groups.includes('dashboard_atemschutz')) return 'Atemschutz';
|
||||||
return 'Mitglied';
|
return 'Mitglied';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,25 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { MonitorHeartOutlined } from '@mui/icons-material';
|
import { MonitorHeartOutlined } from '@mui/icons-material';
|
||||||
import { adminApi } from '../../services/admin';
|
import { adminApi } from '../../services/admin';
|
||||||
import { useCountUp } from '../../hooks/useCountUp';
|
import { useCountUp } from '../../hooks/useCountUp';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { usePermissionContext } from '../../contexts/PermissionContext';
|
||||||
|
|
||||||
function AdminStatusWidget() {
|
function AdminStatusWidget() {
|
||||||
const { user } = useAuth();
|
const { hasPermission } = usePermissionContext();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const isAdmin = user?.groups?.includes('dashboard_admin') ?? false;
|
const canView = hasPermission('admin:view');
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ['admin-status-summary'],
|
queryKey: ['admin-status-summary'],
|
||||||
queryFn: () => adminApi.getStatusSummary(),
|
queryFn: () => adminApi.getStatusSummary(),
|
||||||
refetchInterval: 30_000,
|
refetchInterval: 30_000,
|
||||||
enabled: isAdmin,
|
enabled: canView,
|
||||||
});
|
});
|
||||||
|
|
||||||
const up = useCountUp(data?.up ?? 0);
|
const up = useCountUp(data?.up ?? 0);
|
||||||
const total = useCountUp(data?.total ?? 0);
|
const total = useCountUp(data?.total ?? 0);
|
||||||
|
|
||||||
if (!isAdmin) return null;
|
if (!canView) return null;
|
||||||
|
|
||||||
const allUp = data && data.up === data.total;
|
const allUp = data && data.up === data.total;
|
||||||
const majorityDown = data && data.total > 0 && data.up < data.total / 2;
|
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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||||
import { useNotification } from '../contexts/NotificationContext';
|
import { useNotification } from '../contexts/NotificationContext';
|
||||||
import { settingsApi } from '../services/settings';
|
import { settingsApi } from '../services/settings';
|
||||||
|
|
||||||
@@ -61,11 +61,11 @@ const ADMIN_INTERVAL_OPTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function AdminSettings() {
|
function AdminSettings() {
|
||||||
const { user } = useAuth();
|
const { hasPermission } = usePermissionContext();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const isAdmin = user?.groups?.includes('dashboard_admin') ?? false;
|
const canAccess = hasPermission('admin:write');
|
||||||
|
|
||||||
// State for link collections
|
// State for link collections
|
||||||
const [linkCollections, setLinkCollections] = useState<LinkCollection[]>([]);
|
const [linkCollections, setLinkCollections] = useState<LinkCollection[]>([]);
|
||||||
@@ -86,7 +86,7 @@ function AdminSettings() {
|
|||||||
const { data: settings, isLoading } = useQuery({
|
const { data: settings, isLoading } = useQuery({
|
||||||
queryKey: ['admin-settings'],
|
queryKey: ['admin-settings'],
|
||||||
queryFn: () => settingsApi.getAll(),
|
queryFn: () => settingsApi.getAll(),
|
||||||
enabled: isAdmin,
|
enabled: canAccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize state from fetched settings
|
// Initialize state from fetched settings
|
||||||
@@ -197,7 +197,7 @@ function AdminSettings() {
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!canAccess) {
|
||||||
return <Navigate to="/dashboard" replace />;
|
return <Navigate to="/dashboard" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import {
|
|||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||||
import { trainingApi } from '../services/training';
|
import { trainingApi } from '../services/training';
|
||||||
import type { TeilnahmeStatus, UebungTyp, Teilnahme } from '../types/training.types';
|
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`;
|
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
|
// RSVP Status icon
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -199,7 +187,7 @@ function MarkAttendanceDialog({
|
|||||||
function AttendeeAccordion({
|
function AttendeeAccordion({
|
||||||
teilnahmen,
|
teilnahmen,
|
||||||
counts,
|
counts,
|
||||||
userRole,
|
canSeeList,
|
||||||
}: {
|
}: {
|
||||||
teilnahmen?: Teilnahme[];
|
teilnahmen?: Teilnahme[];
|
||||||
counts: {
|
counts: {
|
||||||
@@ -210,9 +198,8 @@ function AttendeeAccordion({
|
|||||||
anzahl_erschienen: number;
|
anzahl_erschienen: number;
|
||||||
gesamt_eingeladen: number;
|
gesamt_eingeladen: number;
|
||||||
};
|
};
|
||||||
userRole?: string;
|
canSeeList: boolean;
|
||||||
}) {
|
}) {
|
||||||
const canSeeList = hasRole(userRole, 'gruppenfuehrer');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Accordion disableGutters elevation={0} sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
|
<Accordion disableGutters elevation={0} sx={{ border: '1px solid', borderColor: 'divider', borderRadius: 1 }}>
|
||||||
@@ -279,14 +266,13 @@ function AttendeeAccordion({
|
|||||||
export default function UebungDetail() {
|
export default function UebungDetail() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuth();
|
const { hasPermission } = usePermissionContext();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
// We cast user to include `role` (added by Tier 1)
|
const canWrite = hasPermission('kalender:create');
|
||||||
const userRole = (user as any)?.role as string | undefined;
|
const canSeeAttendees = hasPermission('kalender:mark_attendance');
|
||||||
const canWrite = hasRole(userRole, 'gruppenfuehrer');
|
|
||||||
|
|
||||||
const [markAttendanceOpen, setMarkAttendanceOpen] = useState(false);
|
const [markAttendanceOpen, setMarkAttendanceOpen] = useState(false);
|
||||||
const [rsvpLoading, setRsvpLoading] = useState<'zugesagt' | 'abgesagt' | null>(null);
|
const [rsvpLoading, setRsvpLoading] = useState<'zugesagt' | 'abgesagt' | null>(null);
|
||||||
@@ -531,7 +517,7 @@ export default function UebungDetail() {
|
|||||||
<AttendeeAccordion
|
<AttendeeAccordion
|
||||||
teilnahmen={event.teilnahmen}
|
teilnahmen={event.teilnahmen}
|
||||||
counts={event}
|
counts={event}
|
||||||
userRole={userRole}
|
canSeeList={canSeeAttendees}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user