rights system
This commit is contained in:
@@ -2,7 +2,7 @@ import { Request, Response } from 'express';
|
|||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import bookingService from '../services/booking.service';
|
import bookingService from '../services/booking.service';
|
||||||
import vehicleService from '../services/vehicle.service';
|
import vehicleService from '../services/vehicle.service';
|
||||||
import { hasPermission, resolveRequestRole } from '../middleware/rbac.middleware';
|
import { permissionService } from '../services/permission.service';
|
||||||
import {
|
import {
|
||||||
CreateBuchungSchema,
|
CreateBuchungSchema,
|
||||||
UpdateBuchungSchema,
|
UpdateBuchungSchema,
|
||||||
@@ -217,15 +217,19 @@ class BookingController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check ownership: creator can always cancel their own booking
|
// Check ownership: creator can cancel if they have cancel_own_bookings permission
|
||||||
const booking = await bookingService.getById(id);
|
const booking = await bookingService.getById(id);
|
||||||
if (!booking) {
|
if (!booking) {
|
||||||
res.status(404).json({ success: false, message: 'Buchung nicht gefunden' });
|
res.status(404).json({ success: false, message: 'Buchung nicht gefunden' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const isOwner = booking.gebucht_von === req.user!.id;
|
const isOwner = booking.gebucht_von === req.user!.id;
|
||||||
const role = resolveRequestRole(req);
|
const groups: string[] = req.user?.groups ?? [];
|
||||||
if (!isOwner && !hasPermission(role, 'bookings:write')) {
|
const isAdmin = groups.includes('dashboard_admin');
|
||||||
|
const canCancelOwn = isAdmin || permissionService.hasPermission(groups, 'kalender:cancel_own_bookings');
|
||||||
|
const canCancelAny = isAdmin || permissionService.hasPermission(groups, 'kalender:delete_bookings');
|
||||||
|
|
||||||
|
if (!(isOwner && canCancelOwn) && !canCancelAny) {
|
||||||
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
|
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,20 @@ class PermissionController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/admin/permissions/unknown-groups
|
||||||
|
* Returns Authentik groups found in users table but not in the permission matrix.
|
||||||
|
*/
|
||||||
|
async getUnknownGroups(_req: Request, res: Response): Promise<void> {
|
||||||
|
try {
|
||||||
|
const groups = await permissionService.getUnknownGroups();
|
||||||
|
res.json({ success: true, data: groups });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to get unknown groups', { error });
|
||||||
|
res.status(500).json({ success: false, message: 'Fehler beim Laden der unbekannten Gruppen' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /api/admin/permissions/maintenance/:featureGroupId
|
* PUT /api/admin/permissions/maintenance/:featureGroupId
|
||||||
* Toggles maintenance mode for a feature group.
|
* Toggles maintenance mode for a feature group.
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
-- Migration 037: DB-driven permission system
|
-- Migration 037: DB-driven permission system
|
||||||
-- Replaces hardcoded RBAC with per-Authentik-group permission assignments
|
-- Replaces hardcoded RBAC with per-Authentik-group permission assignments
|
||||||
|
-- DROP + recreate is safe because this feature has not been deployed yet.
|
||||||
|
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- 0. Clean slate
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS group_permissions CASCADE;
|
||||||
|
DROP TABLE IF EXISTS permissions CASCADE;
|
||||||
|
DROP TABLE IF EXISTS feature_groups CASCADE;
|
||||||
|
|
||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
-- 1. Feature Groups
|
-- 1. Feature Groups
|
||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS feature_groups (
|
CREATE TABLE feature_groups (
|
||||||
id VARCHAR(50) PRIMARY KEY,
|
id VARCHAR(50) PRIMARY KEY,
|
||||||
label VARCHAR(100) NOT NULL,
|
label VARCHAR(100) NOT NULL,
|
||||||
sort_order INT NOT NULL DEFAULT 0,
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
@@ -21,15 +30,15 @@ INSERT INTO feature_groups (id, label, sort_order) VALUES
|
|||||||
('atemschutz', 'Atemschutz', 6),
|
('atemschutz', 'Atemschutz', 6),
|
||||||
('wissen', 'Wissen', 7),
|
('wissen', 'Wissen', 7),
|
||||||
('vikunja', 'Vikunja', 8),
|
('vikunja', 'Vikunja', 8),
|
||||||
('nextcloud', 'Nextcloud', 9),
|
('dashboard', 'Dashboard', 9),
|
||||||
('dashboard', 'Dashboard', 10)
|
('admin', 'Admin', 10)
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
-- 2. Permissions
|
-- 2. Permissions
|
||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS permissions (
|
CREATE TABLE permissions (
|
||||||
id VARCHAR(100) PRIMARY KEY,
|
id VARCHAR(100) PRIMARY KEY,
|
||||||
feature_group_id VARCHAR(50) NOT NULL REFERENCES feature_groups(id) ON DELETE CASCADE,
|
feature_group_id VARCHAR(50) NOT NULL REFERENCES feature_groups(id) ON DELETE CASCADE,
|
||||||
label VARCHAR(150) NOT NULL,
|
label VARCHAR(150) NOT NULL,
|
||||||
@@ -39,90 +48,77 @@ CREATE TABLE IF NOT EXISTS permissions (
|
|||||||
|
|
||||||
-- Kalender permissions
|
-- Kalender permissions
|
||||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||||
('kalender:access', 'kalender', 'Zugriff', 'Kalender-Seite anzeigen', 1),
|
('kalender:view', 'kalender', 'Ansehen', 'Kalender einsehen (Termine, Übungen, Buchungen)', 1),
|
||||||
('kalender:view_events', 'kalender', 'Termine ansehen', 'Termine und Übungen einsehen', 2),
|
('kalender:create', 'kalender', 'Erstellen', 'Termine und Übungen erstellen', 2),
|
||||||
('kalender:view_bookings', 'kalender', 'Buchungen ansehen', 'Fahrzeugbuchungen einsehen', 3),
|
('kalender:cancel', 'kalender', 'Absagen', 'Termine und Übungen absagen', 3),
|
||||||
('kalender:create_events', 'kalender', 'Termine erstellen', 'Neue Termine/Veranstaltungen erstellen', 4),
|
('kalender:mark_attendance', 'kalender', 'Anwesenheit eintragen', 'Teilnahme bestätigen', 4),
|
||||||
('kalender:create_training', 'kalender', 'Übungen erstellen', 'Neue Übungen anlegen', 5),
|
('kalender:create_bookings', 'kalender', 'Buchungen erstellen', 'Neue Fahrzeugbuchungen anlegen', 5),
|
||||||
('kalender:cancel_training', 'kalender', 'Übungen absagen', 'Übungen absagen/löschen', 6),
|
('kalender:edit_bookings', 'kalender', 'Buchungen bearbeiten', 'Bestehende Buchungen ändern', 6),
|
||||||
('kalender:mark_attendance', 'kalender', 'Anwesenheit eintragen', 'Teilnahme an Übungen bestätigen', 7),
|
('kalender:cancel_own_bookings','kalender', 'Eigene Buchungen stornieren','Eigene Buchungen stornieren', 7),
|
||||||
('kalender:create_bookings', 'kalender', 'Buchungen erstellen', 'Neue Fahrzeugbuchungen anlegen', 8),
|
('kalender:delete_bookings', 'kalender', 'Alle Buchungen stornieren/löschen', 'Alle Buchungen stornieren oder löschen', 8),
|
||||||
('kalender:edit_bookings', 'kalender', 'Buchungen bearbeiten', 'Bestehende Buchungen ändern', 9),
|
('kalender:manage_categories', 'kalender', 'Kategorien verwalten', 'Veranstaltungskategorien verwalten', 9),
|
||||||
('kalender:delete_bookings', 'kalender', 'Buchungen löschen', 'Buchungen endgültig löschen', 10),
|
('kalender:view_reports', 'kalender', 'Berichte ansehen', 'Übungsstatistiken und Berichte einsehen', 10),
|
||||||
('kalender:manage_categories', 'kalender', 'Kategorien verwalten', 'Veranstaltungskategorien verwalten', 11),
|
('kalender:widget_events', 'kalender', 'Widget: Termine', 'Dashboard-Widget für Termine', 11),
|
||||||
('kalender:view_reports', 'kalender', 'Berichte ansehen', 'Übungsstatistiken und Berichte einsehen', 12),
|
('kalender:widget_bookings', 'kalender', 'Widget: Buchungen', 'Dashboard-Widget für Buchungen', 12),
|
||||||
('kalender:widget_events', 'kalender', 'Widget: Termine', 'Dashboard-Widget für Termine', 13),
|
('kalender:widget_quick_add', 'kalender', 'Widget: Schnell-Termin', 'Dashboard-Widget zum schnellen Erstellen', 13)
|
||||||
('kalender:widget_bookings', 'kalender', 'Widget: Buchungen', 'Dashboard-Widget für Buchungen', 14),
|
|
||||||
('kalender:widget_quick_add', 'kalender', 'Widget: Schnell-Termin', 'Dashboard-Widget zum schnellen Erstellen', 15)
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- Fahrzeuge permissions
|
-- Fahrzeuge permissions
|
||||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||||
('fahrzeuge:access', 'fahrzeuge', 'Zugriff', 'Fahrzeug-Seite anzeigen', 1),
|
('fahrzeuge:view', 'fahrzeuge', 'Ansehen', 'Fahrzeugdetails einsehen', 1),
|
||||||
('fahrzeuge:view', 'fahrzeuge', 'Ansehen', 'Fahrzeugdetails einsehen', 2),
|
('fahrzeuge:create', 'fahrzeuge', 'Erstellen', 'Neue Fahrzeuge anlegen', 2),
|
||||||
('fahrzeuge:create', 'fahrzeuge', 'Erstellen', 'Neue Fahrzeuge anlegen', 3),
|
('fahrzeuge:change_status', 'fahrzeuge', 'Status ändern', 'Fahrzeugstatus ändern', 3),
|
||||||
('fahrzeuge:change_status', 'fahrzeuge', 'Status ändern', 'Fahrzeugstatus ändern', 4),
|
('fahrzeuge:manage_maintenance', 'fahrzeuge', 'Wartung verwalten', 'Wartungseinträge erstellen/bearbeiten', 4),
|
||||||
('fahrzeuge:manage_maintenance', 'fahrzeuge', 'Wartung verwalten', 'Wartungseinträge erstellen/bearbeiten', 5),
|
('fahrzeuge:delete', 'fahrzeuge', 'Löschen', 'Fahrzeuge löschen', 5),
|
||||||
('fahrzeuge:delete', 'fahrzeuge', 'Löschen', 'Fahrzeuge löschen', 6),
|
('fahrzeuge:widget', 'fahrzeuge', 'Widget', 'Dashboard-Widget für Fahrzeuge', 6)
|
||||||
('fahrzeuge:widget', 'fahrzeuge', 'Widget', 'Dashboard-Widget für Fahrzeuge', 7)
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- Einsätze permissions
|
-- Einsätze permissions
|
||||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||||
('einsaetze:access', 'einsaetze', 'Zugriff', 'Einsatz-Seite anzeigen', 1),
|
('einsaetze:view', 'einsaetze', 'Ansehen', 'Einsätze einsehen', 1),
|
||||||
('einsaetze:view', 'einsaetze', 'Ansehen', 'Einsätze einsehen', 2),
|
('einsaetze:view_reports', 'einsaetze', 'Berichte ansehen', 'Einsatzberichte/Berichtstext einsehen', 2),
|
||||||
('einsaetze:view_reports', 'einsaetze', 'Berichte ansehen', 'Einsatzberichte/Berichtstext einsehen', 3),
|
('einsaetze:create', 'einsaetze', 'Erstellen', 'Neue Einsätze anlegen und bearbeiten', 3),
|
||||||
('einsaetze:create', 'einsaetze', 'Erstellen', 'Neue Einsätze anlegen und bearbeiten', 4),
|
('einsaetze:delete', 'einsaetze', 'Löschen', 'Einsätze archivieren/löschen', 4),
|
||||||
('einsaetze:delete', 'einsaetze', 'Löschen', 'Einsätze archivieren/löschen', 5),
|
('einsaetze:manage_personnel', 'einsaetze', 'Personal verwalten', 'Einsatzpersonal und Fahrzeuge zuweisen', 5)
|
||||||
('einsaetze:manage_personnel', 'einsaetze', 'Personal verwalten', 'Einsatzpersonal und Fahrzeuge zuweisen', 6)
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- Ausrüstung permissions
|
-- Ausrüstung permissions
|
||||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||||
('ausruestung:access', 'ausruestung', 'Zugriff', 'Ausrüstungs-Seite anzeigen', 1),
|
('ausruestung:view', 'ausruestung', 'Ansehen', 'Ausrüstung einsehen', 1),
|
||||||
('ausruestung:view', 'ausruestung', 'Ansehen', 'Ausrüstung einsehen', 2),
|
('ausruestung:create', 'ausruestung', 'Erstellen', 'Neue Ausrüstung anlegen und bearbeiten', 2),
|
||||||
('ausruestung:create', 'ausruestung', 'Erstellen', 'Neue Ausrüstung anlegen und bearbeiten', 3),
|
('ausruestung:manage_maintenance', 'ausruestung', 'Wartung verwalten', 'Wartungseinträge verwalten', 3),
|
||||||
('ausruestung:manage_maintenance', 'ausruestung', 'Wartung verwalten', 'Wartungseinträge verwalten', 4),
|
('ausruestung:delete', 'ausruestung', 'Löschen', 'Ausrüstung löschen', 4),
|
||||||
('ausruestung:delete', 'ausruestung', 'Löschen', 'Ausrüstung löschen', 5),
|
('ausruestung:widget', 'ausruestung', 'Widget', 'Dashboard-Widget für Ausrüstung', 5)
|
||||||
('ausruestung:widget', 'ausruestung', 'Widget', 'Dashboard-Widget für Ausrüstung', 6)
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- Mitglieder permissions
|
-- Mitglieder permissions
|
||||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||||
('mitglieder:access', 'mitglieder', 'Zugriff', 'Mitglieder-Seite anzeigen', 1),
|
('mitglieder:view_own', 'mitglieder', 'Eigenes Profil', 'Eigenes Profil einsehen', 1),
|
||||||
('mitglieder:view', 'mitglieder', 'Ansehen', 'Mitglieder-Profile einsehen', 2),
|
('mitglieder:view_all', 'mitglieder', 'Alle Profile', 'Alle Mitglieder-Profile einsehen', 2),
|
||||||
('mitglieder:edit', 'mitglieder', 'Bearbeiten', 'Mitglieder-Profile bearbeiten', 3),
|
('mitglieder:edit', 'mitglieder', 'Bearbeiten', 'Mitglieder-Profile bearbeiten', 3),
|
||||||
('mitglieder:create_profile', 'mitglieder', 'Profil erstellen', 'Neue Mitglieder-Profile anlegen', 4)
|
('mitglieder:create_profile','mitglieder', 'Profil erstellen', 'Neue Mitglieder-Profile anlegen', 4)
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- Atemschutz permissions
|
-- Atemschutz permissions
|
||||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||||
('atemschutz:access', 'atemschutz', 'Zugriff', 'Atemschutz-Seite anzeigen', 1),
|
('atemschutz:view', 'atemschutz', 'Ansehen', 'Atemschutz-Daten aller Träger sehen', 1),
|
||||||
('atemschutz:view', 'atemschutz', 'Ansehen', 'Atemschutz-Daten aller Träger sehen', 2),
|
('atemschutz:create', 'atemschutz', 'Erstellen', 'Atemschutz-Einträge anlegen/ändern', 2),
|
||||||
('atemschutz:create', 'atemschutz', 'Erstellen', 'Atemschutz-Einträge anlegen/ändern', 3),
|
('atemschutz:delete', 'atemschutz', 'Löschen', 'Atemschutz-Einträge löschen', 3),
|
||||||
('atemschutz:delete', 'atemschutz', 'Löschen', 'Atemschutz-Einträge löschen', 4),
|
('atemschutz:widget', 'atemschutz', 'Widget', 'Dashboard-Widget für Atemschutz', 4)
|
||||||
('atemschutz:widget', 'atemschutz', 'Widget', 'Dashboard-Widget für Atemschutz', 5)
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- Wissen permissions
|
-- Wissen permissions
|
||||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||||
('wissen:access', 'wissen', 'Zugriff', 'Wissen-Seite anzeigen', 1),
|
('wissen:view', 'wissen', 'Ansehen', 'Wissen-Seite anzeigen', 1),
|
||||||
('wissen:widget_recent', 'wissen', 'Widget: Letzte', 'Dashboard-Widget letzte Seiten', 2),
|
('wissen:widget_recent', 'wissen', 'Widget: Letzte', 'Dashboard-Widget letzte Seiten', 2),
|
||||||
('wissen:widget_search', 'wissen', 'Widget: Suche', 'Dashboard-Widget für BookStack-Suche', 3)
|
('wissen:widget_search', 'wissen', 'Widget: Suche', 'Dashboard-Widget für BookStack-Suche', 3)
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- Vikunja permissions
|
-- Vikunja permissions
|
||||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||||
('vikunja:access', 'vikunja', 'Zugriff', 'Vikunja-Integration nutzen', 1),
|
('vikunja:create_tasks', 'vikunja', 'Aufgaben erstellen', 'Neue Vikunja-Aufgaben erstellen', 1),
|
||||||
('vikunja:create_tasks', 'vikunja', 'Aufgaben erstellen', 'Neue Vikunja-Aufgaben erstellen', 2),
|
('vikunja:widget_tasks', 'vikunja', 'Widget: Aufgaben', 'Dashboard-Widget für Vikunja-Aufgaben', 2),
|
||||||
('vikunja:widget_tasks', 'vikunja', 'Widget: Aufgaben', 'Dashboard-Widget für Vikunja-Aufgaben', 3),
|
('vikunja:widget_quick_add', 'vikunja', 'Widget: Schnell-Task', 'Dashboard-Widget zum schnellen Erstellen', 3)
|
||||||
('vikunja:widget_quick_add', 'vikunja', 'Widget: Schnell-Task', 'Dashboard-Widget zum schnellen Erstellen', 4)
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
|
||||||
|
|
||||||
-- Nextcloud permissions
|
|
||||||
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
|
||||||
('nextcloud:access', 'nextcloud', 'Zugriff', 'Nextcloud-Integration nutzen', 1),
|
|
||||||
('nextcloud:widget', 'nextcloud', 'Widget', 'Dashboard-Widget für Nextcloud', 2)
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- Dashboard permissions
|
-- Dashboard permissions
|
||||||
@@ -131,11 +127,17 @@ INSERT INTO permissions (id, feature_group_id, label, description, sort_order) V
|
|||||||
('dashboard:widget_banner', 'dashboard', 'Widget: Banner', 'Dashboard-Widget für Banner', 2)
|
('dashboard:widget_banner', 'dashboard', 'Widget: Banner', 'Dashboard-Widget für Banner', 2)
|
||||||
ON CONFLICT (id) DO NOTHING;
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
-- Admin permissions
|
||||||
|
INSERT INTO permissions (id, feature_group_id, label, description, sort_order) VALUES
|
||||||
|
('admin:view', 'admin', 'Ansehen', 'Admin-Panel einsehen', 1),
|
||||||
|
('admin:write', 'admin', 'Bearbeiten', 'Admin-Einstellungen ändern', 2)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
-- 3. Group Permissions
|
-- 3. Group Permissions
|
||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS group_permissions (
|
CREATE TABLE group_permissions (
|
||||||
authentik_group VARCHAR(100) NOT NULL,
|
authentik_group VARCHAR(100) NOT NULL,
|
||||||
permission_id VARCHAR(100) NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
|
permission_id VARCHAR(100) NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
|
||||||
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
@@ -143,296 +145,245 @@ CREATE TABLE IF NOT EXISTS group_permissions (
|
|||||||
PRIMARY KEY (authentik_group, permission_id)
|
PRIMARY KEY (authentik_group, permission_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_group_permissions_group ON group_permissions(authentik_group);
|
CREATE INDEX idx_group_permissions_group ON group_permissions(authentik_group);
|
||||||
|
|
||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
-- 4. Seed data — replicate current RBAC behavior
|
-- 4. Seed data — replicate current RBAC behavior
|
||||||
-- ═══════════════════════════════════════════════════════════════════════════
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- NOTE: dashboard_admin is NOT seeded — it has hardwired full access in code.
|
||||||
|
|
||||||
-- Helper: grant a list of permissions to a group
|
-- ── dashboard_kommando — near-full access ──
|
||||||
-- We insert each combination individually with ON CONFLICT DO NOTHING
|
|
||||||
|
|
||||||
-- ── All authenticated users (mitglied-level) ──
|
|
||||||
-- Every non-bewerber group gets access + view + widget permissions
|
|
||||||
|
|
||||||
-- dashboard_kommando — gets everything (except admin)
|
|
||||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||||
-- Kalender
|
-- Kalender (ALL)
|
||||||
('dashboard_kommando', 'kalender:access'),
|
('dashboard_kommando', 'kalender:view'),
|
||||||
('dashboard_kommando', 'kalender:view_events'),
|
('dashboard_kommando', 'kalender:create'),
|
||||||
('dashboard_kommando', 'kalender:view_bookings'),
|
('dashboard_kommando', 'kalender:cancel'),
|
||||||
('dashboard_kommando', 'kalender:create_events'),
|
|
||||||
('dashboard_kommando', 'kalender:create_training'),
|
|
||||||
('dashboard_kommando', 'kalender:cancel_training'),
|
|
||||||
('dashboard_kommando', 'kalender:mark_attendance'),
|
('dashboard_kommando', 'kalender:mark_attendance'),
|
||||||
('dashboard_kommando', 'kalender:create_bookings'),
|
('dashboard_kommando', 'kalender:create_bookings'),
|
||||||
('dashboard_kommando', 'kalender:edit_bookings'),
|
('dashboard_kommando', 'kalender:edit_bookings'),
|
||||||
|
('dashboard_kommando', 'kalender:cancel_own_bookings'),
|
||||||
('dashboard_kommando', 'kalender:delete_bookings'),
|
('dashboard_kommando', 'kalender:delete_bookings'),
|
||||||
('dashboard_kommando', 'kalender:manage_categories'),
|
('dashboard_kommando', 'kalender:manage_categories'),
|
||||||
('dashboard_kommando', 'kalender:view_reports'),
|
('dashboard_kommando', 'kalender:view_reports'),
|
||||||
('dashboard_kommando', 'kalender:widget_events'),
|
('dashboard_kommando', 'kalender:widget_events'),
|
||||||
('dashboard_kommando', 'kalender:widget_bookings'),
|
('dashboard_kommando', 'kalender:widget_bookings'),
|
||||||
('dashboard_kommando', 'kalender:widget_quick_add'),
|
('dashboard_kommando', 'kalender:widget_quick_add'),
|
||||||
-- Fahrzeuge
|
-- Fahrzeuge (ALL)
|
||||||
('dashboard_kommando', 'fahrzeuge:access'),
|
|
||||||
('dashboard_kommando', 'fahrzeuge:view'),
|
('dashboard_kommando', 'fahrzeuge:view'),
|
||||||
('dashboard_kommando', 'fahrzeuge:create'),
|
('dashboard_kommando', 'fahrzeuge:create'),
|
||||||
('dashboard_kommando', 'fahrzeuge:change_status'),
|
('dashboard_kommando', 'fahrzeuge:change_status'),
|
||||||
('dashboard_kommando', 'fahrzeuge:manage_maintenance'),
|
('dashboard_kommando', 'fahrzeuge:manage_maintenance'),
|
||||||
('dashboard_kommando', 'fahrzeuge:delete'),
|
('dashboard_kommando', 'fahrzeuge:delete'),
|
||||||
('dashboard_kommando', 'fahrzeuge:widget'),
|
('dashboard_kommando', 'fahrzeuge:widget'),
|
||||||
-- Einsätze
|
-- Einsätze (ALL)
|
||||||
('dashboard_kommando', 'einsaetze:access'),
|
|
||||||
('dashboard_kommando', 'einsaetze:view'),
|
('dashboard_kommando', 'einsaetze:view'),
|
||||||
('dashboard_kommando', 'einsaetze:view_reports'),
|
('dashboard_kommando', 'einsaetze:view_reports'),
|
||||||
('dashboard_kommando', 'einsaetze:create'),
|
('dashboard_kommando', 'einsaetze:create'),
|
||||||
('dashboard_kommando', 'einsaetze:delete'),
|
('dashboard_kommando', 'einsaetze:delete'),
|
||||||
('dashboard_kommando', 'einsaetze:manage_personnel'),
|
('dashboard_kommando', 'einsaetze:manage_personnel'),
|
||||||
-- Ausrüstung
|
-- Ausrüstung (ALL)
|
||||||
('dashboard_kommando', 'ausruestung:access'),
|
|
||||||
('dashboard_kommando', 'ausruestung:view'),
|
('dashboard_kommando', 'ausruestung:view'),
|
||||||
('dashboard_kommando', 'ausruestung:create'),
|
('dashboard_kommando', 'ausruestung:create'),
|
||||||
('dashboard_kommando', 'ausruestung:manage_maintenance'),
|
('dashboard_kommando', 'ausruestung:manage_maintenance'),
|
||||||
('dashboard_kommando', 'ausruestung:delete'),
|
('dashboard_kommando', 'ausruestung:delete'),
|
||||||
('dashboard_kommando', 'ausruestung:widget'),
|
('dashboard_kommando', 'ausruestung:widget'),
|
||||||
-- Mitglieder
|
-- Mitglieder (ALL)
|
||||||
('dashboard_kommando', 'mitglieder:access'),
|
('dashboard_kommando', 'mitglieder:view_own'),
|
||||||
('dashboard_kommando', 'mitglieder:view'),
|
('dashboard_kommando', 'mitglieder:view_all'),
|
||||||
('dashboard_kommando', 'mitglieder:edit'),
|
('dashboard_kommando', 'mitglieder:edit'),
|
||||||
('dashboard_kommando', 'mitglieder:create_profile'),
|
('dashboard_kommando', 'mitglieder:create_profile'),
|
||||||
-- Atemschutz
|
-- Atemschutz (ALL)
|
||||||
('dashboard_kommando', 'atemschutz:access'),
|
|
||||||
('dashboard_kommando', 'atemschutz:view'),
|
('dashboard_kommando', 'atemschutz:view'),
|
||||||
('dashboard_kommando', 'atemschutz:create'),
|
('dashboard_kommando', 'atemschutz:create'),
|
||||||
('dashboard_kommando', 'atemschutz:delete'),
|
('dashboard_kommando', 'atemschutz:delete'),
|
||||||
('dashboard_kommando', 'atemschutz:widget'),
|
('dashboard_kommando', 'atemschutz:widget'),
|
||||||
-- Wissen
|
-- Wissen (ALL)
|
||||||
('dashboard_kommando', 'wissen:access'),
|
('dashboard_kommando', 'wissen:view'),
|
||||||
('dashboard_kommando', 'wissen:widget_recent'),
|
('dashboard_kommando', 'wissen:widget_recent'),
|
||||||
('dashboard_kommando', 'wissen:widget_search'),
|
('dashboard_kommando', 'wissen:widget_search'),
|
||||||
-- Vikunja
|
-- Vikunja (ALL)
|
||||||
('dashboard_kommando', 'vikunja:access'),
|
|
||||||
('dashboard_kommando', 'vikunja:create_tasks'),
|
('dashboard_kommando', 'vikunja:create_tasks'),
|
||||||
('dashboard_kommando', 'vikunja:widget_tasks'),
|
('dashboard_kommando', 'vikunja:widget_tasks'),
|
||||||
('dashboard_kommando', 'vikunja:widget_quick_add'),
|
('dashboard_kommando', 'vikunja:widget_quick_add'),
|
||||||
-- Nextcloud
|
-- Dashboard (ALL)
|
||||||
('dashboard_kommando', 'nextcloud:access'),
|
|
||||||
('dashboard_kommando', 'nextcloud:widget'),
|
|
||||||
-- Dashboard
|
|
||||||
('dashboard_kommando', 'dashboard:widget_links'),
|
('dashboard_kommando', 'dashboard:widget_links'),
|
||||||
('dashboard_kommando', 'dashboard:widget_banner')
|
('dashboard_kommando', 'dashboard:widget_banner'),
|
||||||
|
-- Admin (view only)
|
||||||
|
('dashboard_kommando', 'admin:view')
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- dashboard_gruppenfuehrer — write-level for most features
|
-- ── dashboard_gruppenfuehrer — write level for most ──
|
||||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||||
-- Kalender
|
-- Kalender
|
||||||
('dashboard_gruppenfuehrer', 'kalender:access'),
|
('dashboard_gruppenfuehrer', 'kalender:view'),
|
||||||
('dashboard_gruppenfuehrer', 'kalender:view_events'),
|
('dashboard_gruppenfuehrer', 'kalender:create'),
|
||||||
('dashboard_gruppenfuehrer', 'kalender:view_bookings'),
|
|
||||||
('dashboard_gruppenfuehrer', 'kalender:create_events'),
|
|
||||||
('dashboard_gruppenfuehrer', 'kalender:create_training'),
|
|
||||||
('dashboard_gruppenfuehrer', 'kalender:mark_attendance'),
|
('dashboard_gruppenfuehrer', 'kalender:mark_attendance'),
|
||||||
('dashboard_gruppenfuehrer', 'kalender:create_bookings'),
|
('dashboard_gruppenfuehrer', 'kalender:create_bookings'),
|
||||||
('dashboard_gruppenfuehrer', 'kalender:edit_bookings'),
|
('dashboard_gruppenfuehrer', 'kalender:edit_bookings'),
|
||||||
|
('dashboard_gruppenfuehrer', 'kalender:cancel_own_bookings'),
|
||||||
('dashboard_gruppenfuehrer', 'kalender:manage_categories'),
|
('dashboard_gruppenfuehrer', 'kalender:manage_categories'),
|
||||||
('dashboard_gruppenfuehrer', 'kalender:widget_events'),
|
('dashboard_gruppenfuehrer', 'kalender:widget_events'),
|
||||||
('dashboard_gruppenfuehrer', 'kalender:widget_bookings'),
|
('dashboard_gruppenfuehrer', 'kalender:widget_bookings'),
|
||||||
('dashboard_gruppenfuehrer', 'kalender:widget_quick_add'),
|
('dashboard_gruppenfuehrer', 'kalender:widget_quick_add'),
|
||||||
-- Fahrzeuge
|
-- Fahrzeuge
|
||||||
('dashboard_gruppenfuehrer', 'fahrzeuge:access'),
|
|
||||||
('dashboard_gruppenfuehrer', 'fahrzeuge:view'),
|
('dashboard_gruppenfuehrer', 'fahrzeuge:view'),
|
||||||
('dashboard_gruppenfuehrer', 'fahrzeuge:change_status'),
|
('dashboard_gruppenfuehrer', 'fahrzeuge:change_status'),
|
||||||
('dashboard_gruppenfuehrer', 'fahrzeuge:manage_maintenance'),
|
('dashboard_gruppenfuehrer', 'fahrzeuge:manage_maintenance'),
|
||||||
('dashboard_gruppenfuehrer', 'fahrzeuge:widget'),
|
('dashboard_gruppenfuehrer', 'fahrzeuge:widget'),
|
||||||
-- Einsätze
|
-- Einsätze
|
||||||
('dashboard_gruppenfuehrer', 'einsaetze:access'),
|
|
||||||
('dashboard_gruppenfuehrer', 'einsaetze:view'),
|
('dashboard_gruppenfuehrer', 'einsaetze:view'),
|
||||||
('dashboard_gruppenfuehrer', 'einsaetze:create'),
|
('dashboard_gruppenfuehrer', 'einsaetze:create'),
|
||||||
('dashboard_gruppenfuehrer', 'einsaetze:manage_personnel'),
|
('dashboard_gruppenfuehrer', 'einsaetze:manage_personnel'),
|
||||||
-- Ausrüstung
|
-- Ausrüstung
|
||||||
('dashboard_gruppenfuehrer', 'ausruestung:access'),
|
|
||||||
('dashboard_gruppenfuehrer', 'ausruestung:view'),
|
('dashboard_gruppenfuehrer', 'ausruestung:view'),
|
||||||
('dashboard_gruppenfuehrer', 'ausruestung:create'),
|
('dashboard_gruppenfuehrer', 'ausruestung:create'),
|
||||||
('dashboard_gruppenfuehrer', 'ausruestung:manage_maintenance'),
|
('dashboard_gruppenfuehrer', 'ausruestung:manage_maintenance'),
|
||||||
('dashboard_gruppenfuehrer', 'ausruestung:widget'),
|
('dashboard_gruppenfuehrer', 'ausruestung:widget'),
|
||||||
-- Mitglieder
|
-- Mitglieder
|
||||||
('dashboard_gruppenfuehrer', 'mitglieder:access'),
|
('dashboard_gruppenfuehrer', 'mitglieder:view_own'),
|
||||||
('dashboard_gruppenfuehrer', 'mitglieder:view'),
|
('dashboard_gruppenfuehrer', 'mitglieder:view_all'),
|
||||||
-- Atemschutz
|
-- Atemschutz
|
||||||
('dashboard_gruppenfuehrer', 'atemschutz:access'),
|
|
||||||
('dashboard_gruppenfuehrer', 'atemschutz:view'),
|
('dashboard_gruppenfuehrer', 'atemschutz:view'),
|
||||||
('dashboard_gruppenfuehrer', 'atemschutz:create'),
|
('dashboard_gruppenfuehrer', 'atemschutz:create'),
|
||||||
('dashboard_gruppenfuehrer', 'atemschutz:widget'),
|
('dashboard_gruppenfuehrer', 'atemschutz:widget'),
|
||||||
-- Wissen
|
-- Wissen
|
||||||
('dashboard_gruppenfuehrer', 'wissen:access'),
|
('dashboard_gruppenfuehrer', 'wissen:view'),
|
||||||
('dashboard_gruppenfuehrer', 'wissen:widget_recent'),
|
('dashboard_gruppenfuehrer', 'wissen:widget_recent'),
|
||||||
('dashboard_gruppenfuehrer', 'wissen:widget_search'),
|
('dashboard_gruppenfuehrer', 'wissen:widget_search'),
|
||||||
-- Vikunja
|
-- Vikunja
|
||||||
('dashboard_gruppenfuehrer', 'vikunja:access'),
|
|
||||||
('dashboard_gruppenfuehrer', 'vikunja:create_tasks'),
|
('dashboard_gruppenfuehrer', 'vikunja:create_tasks'),
|
||||||
('dashboard_gruppenfuehrer', 'vikunja:widget_tasks'),
|
('dashboard_gruppenfuehrer', 'vikunja:widget_tasks'),
|
||||||
('dashboard_gruppenfuehrer', 'vikunja:widget_quick_add'),
|
('dashboard_gruppenfuehrer', 'vikunja:widget_quick_add'),
|
||||||
-- Nextcloud
|
|
||||||
('dashboard_gruppenfuehrer', 'nextcloud:access'),
|
|
||||||
('dashboard_gruppenfuehrer', 'nextcloud:widget'),
|
|
||||||
-- Dashboard
|
-- Dashboard
|
||||||
('dashboard_gruppenfuehrer', 'dashboard:widget_links'),
|
('dashboard_gruppenfuehrer', 'dashboard:widget_links'),
|
||||||
('dashboard_gruppenfuehrer', 'dashboard:widget_banner')
|
('dashboard_gruppenfuehrer', 'dashboard:widget_banner')
|
||||||
ON CONFLICT DO NOTHING;
|
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
|
||||||
('dashboard_fahrmeister', 'kalender:access'),
|
('dashboard_fahrmeister', 'kalender:view'),
|
||||||
('dashboard_fahrmeister', 'kalender:view_events'),
|
|
||||||
('dashboard_fahrmeister', 'kalender:view_bookings'),
|
|
||||||
('dashboard_fahrmeister', 'kalender:create_bookings'),
|
('dashboard_fahrmeister', 'kalender:create_bookings'),
|
||||||
('dashboard_fahrmeister', 'kalender:edit_bookings'),
|
('dashboard_fahrmeister', 'kalender:edit_bookings'),
|
||||||
|
('dashboard_fahrmeister', 'kalender:cancel_own_bookings'),
|
||||||
('dashboard_fahrmeister', 'kalender:widget_events'),
|
('dashboard_fahrmeister', 'kalender:widget_events'),
|
||||||
('dashboard_fahrmeister', 'kalender:widget_bookings'),
|
('dashboard_fahrmeister', 'kalender:widget_bookings'),
|
||||||
-- Fahrzeuge
|
-- Fahrzeuge
|
||||||
('dashboard_fahrmeister', 'fahrzeuge:access'),
|
|
||||||
('dashboard_fahrmeister', 'fahrzeuge:view'),
|
('dashboard_fahrmeister', 'fahrzeuge:view'),
|
||||||
('dashboard_fahrmeister', 'fahrzeuge:change_status'),
|
('dashboard_fahrmeister', 'fahrzeuge:change_status'),
|
||||||
('dashboard_fahrmeister', 'fahrzeuge:manage_maintenance'),
|
('dashboard_fahrmeister', 'fahrzeuge:manage_maintenance'),
|
||||||
('dashboard_fahrmeister', 'fahrzeuge:widget'),
|
('dashboard_fahrmeister', 'fahrzeuge:widget'),
|
||||||
-- Einsätze
|
-- Einsätze
|
||||||
('dashboard_fahrmeister', 'einsaetze:access'),
|
|
||||||
('dashboard_fahrmeister', 'einsaetze:view'),
|
('dashboard_fahrmeister', 'einsaetze:view'),
|
||||||
-- Ausrüstung
|
-- Ausrüstung
|
||||||
('dashboard_fahrmeister', 'ausruestung:access'),
|
|
||||||
('dashboard_fahrmeister', 'ausruestung:view'),
|
('dashboard_fahrmeister', 'ausruestung:view'),
|
||||||
('dashboard_fahrmeister', 'ausruestung:create'),
|
('dashboard_fahrmeister', 'ausruestung:create'),
|
||||||
('dashboard_fahrmeister', 'ausruestung:manage_maintenance'),
|
('dashboard_fahrmeister', 'ausruestung:manage_maintenance'),
|
||||||
('dashboard_fahrmeister', 'ausruestung:widget'),
|
('dashboard_fahrmeister', 'ausruestung:widget'),
|
||||||
-- Mitglieder
|
-- Mitglieder
|
||||||
('dashboard_fahrmeister', 'mitglieder:access'),
|
('dashboard_fahrmeister', 'mitglieder:view_own'),
|
||||||
('dashboard_fahrmeister', 'mitglieder:view'),
|
('dashboard_fahrmeister', 'mitglieder:view_all'),
|
||||||
-- Atemschutz
|
-- Atemschutz
|
||||||
('dashboard_fahrmeister', 'atemschutz:access'),
|
|
||||||
('dashboard_fahrmeister', 'atemschutz:widget'),
|
('dashboard_fahrmeister', 'atemschutz:widget'),
|
||||||
-- Wissen
|
-- Wissen
|
||||||
('dashboard_fahrmeister', 'wissen:access'),
|
('dashboard_fahrmeister', 'wissen:view'),
|
||||||
('dashboard_fahrmeister', 'wissen:widget_recent'),
|
('dashboard_fahrmeister', 'wissen:widget_recent'),
|
||||||
('dashboard_fahrmeister', 'wissen:widget_search'),
|
('dashboard_fahrmeister', 'wissen:widget_search'),
|
||||||
-- Vikunja
|
-- Vikunja
|
||||||
('dashboard_fahrmeister', 'vikunja:access'),
|
|
||||||
('dashboard_fahrmeister', 'vikunja:create_tasks'),
|
('dashboard_fahrmeister', 'vikunja:create_tasks'),
|
||||||
('dashboard_fahrmeister', 'vikunja:widget_tasks'),
|
('dashboard_fahrmeister', 'vikunja:widget_tasks'),
|
||||||
('dashboard_fahrmeister', 'vikunja:widget_quick_add'),
|
('dashboard_fahrmeister', 'vikunja:widget_quick_add'),
|
||||||
-- Nextcloud
|
|
||||||
('dashboard_fahrmeister', 'nextcloud:access'),
|
|
||||||
('dashboard_fahrmeister', 'nextcloud:widget'),
|
|
||||||
-- Dashboard
|
-- Dashboard
|
||||||
('dashboard_fahrmeister', 'dashboard:widget_links'),
|
('dashboard_fahrmeister', 'dashboard:widget_links'),
|
||||||
('dashboard_fahrmeister', 'dashboard:widget_banner')
|
('dashboard_fahrmeister', 'dashboard:widget_banner')
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- dashboard_zeugmeister — equipment specialist
|
-- ── dashboard_zeugmeister — equipment specialist ──
|
||||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||||
-- Kalender
|
-- Kalender
|
||||||
('dashboard_zeugmeister', 'kalender:access'),
|
('dashboard_zeugmeister', 'kalender:view'),
|
||||||
('dashboard_zeugmeister', 'kalender:view_events'),
|
|
||||||
('dashboard_zeugmeister', 'kalender:view_bookings'),
|
|
||||||
('dashboard_zeugmeister', 'kalender:create_bookings'),
|
('dashboard_zeugmeister', 'kalender:create_bookings'),
|
||||||
|
('dashboard_zeugmeister', 'kalender:cancel_own_bookings'),
|
||||||
('dashboard_zeugmeister', 'kalender:widget_events'),
|
('dashboard_zeugmeister', 'kalender:widget_events'),
|
||||||
('dashboard_zeugmeister', 'kalender:widget_bookings'),
|
('dashboard_zeugmeister', 'kalender:widget_bookings'),
|
||||||
-- Fahrzeuge
|
-- Fahrzeuge
|
||||||
('dashboard_zeugmeister', 'fahrzeuge:access'),
|
|
||||||
('dashboard_zeugmeister', 'fahrzeuge:view'),
|
('dashboard_zeugmeister', 'fahrzeuge:view'),
|
||||||
('dashboard_zeugmeister', 'fahrzeuge:change_status'),
|
('dashboard_zeugmeister', 'fahrzeuge:change_status'),
|
||||||
('dashboard_zeugmeister', 'fahrzeuge:manage_maintenance'),
|
('dashboard_zeugmeister', 'fahrzeuge:manage_maintenance'),
|
||||||
('dashboard_zeugmeister', 'fahrzeuge:widget'),
|
('dashboard_zeugmeister', 'fahrzeuge:widget'),
|
||||||
-- Einsätze
|
-- Einsätze
|
||||||
('dashboard_zeugmeister', 'einsaetze:access'),
|
|
||||||
('dashboard_zeugmeister', 'einsaetze:view'),
|
('dashboard_zeugmeister', 'einsaetze:view'),
|
||||||
-- Ausrüstung
|
-- Ausrüstung
|
||||||
('dashboard_zeugmeister', 'ausruestung:access'),
|
|
||||||
('dashboard_zeugmeister', 'ausruestung:view'),
|
('dashboard_zeugmeister', 'ausruestung:view'),
|
||||||
('dashboard_zeugmeister', 'ausruestung:create'),
|
('dashboard_zeugmeister', 'ausruestung:create'),
|
||||||
('dashboard_zeugmeister', 'ausruestung:manage_maintenance'),
|
('dashboard_zeugmeister', 'ausruestung:manage_maintenance'),
|
||||||
('dashboard_zeugmeister', 'ausruestung:widget'),
|
('dashboard_zeugmeister', 'ausruestung:widget'),
|
||||||
-- Mitglieder
|
-- Mitglieder
|
||||||
('dashboard_zeugmeister', 'mitglieder:access'),
|
('dashboard_zeugmeister', 'mitglieder:view_own'),
|
||||||
('dashboard_zeugmeister', 'mitglieder:view'),
|
('dashboard_zeugmeister', 'mitglieder:view_all'),
|
||||||
-- Atemschutz
|
-- Atemschutz
|
||||||
('dashboard_zeugmeister', 'atemschutz:access'),
|
|
||||||
('dashboard_zeugmeister', 'atemschutz:widget'),
|
('dashboard_zeugmeister', 'atemschutz:widget'),
|
||||||
-- Wissen
|
-- Wissen
|
||||||
('dashboard_zeugmeister', 'wissen:access'),
|
('dashboard_zeugmeister', 'wissen:view'),
|
||||||
('dashboard_zeugmeister', 'wissen:widget_recent'),
|
('dashboard_zeugmeister', 'wissen:widget_recent'),
|
||||||
('dashboard_zeugmeister', 'wissen:widget_search'),
|
('dashboard_zeugmeister', 'wissen:widget_search'),
|
||||||
-- Vikunja
|
-- Vikunja
|
||||||
('dashboard_zeugmeister', 'vikunja:access'),
|
|
||||||
('dashboard_zeugmeister', 'vikunja:create_tasks'),
|
('dashboard_zeugmeister', 'vikunja:create_tasks'),
|
||||||
('dashboard_zeugmeister', 'vikunja:widget_tasks'),
|
('dashboard_zeugmeister', 'vikunja:widget_tasks'),
|
||||||
('dashboard_zeugmeister', 'vikunja:widget_quick_add'),
|
('dashboard_zeugmeister', 'vikunja:widget_quick_add'),
|
||||||
-- Nextcloud
|
|
||||||
('dashboard_zeugmeister', 'nextcloud:access'),
|
|
||||||
('dashboard_zeugmeister', 'nextcloud:widget'),
|
|
||||||
-- Dashboard
|
-- Dashboard
|
||||||
('dashboard_zeugmeister', 'dashboard:widget_links'),
|
('dashboard_zeugmeister', 'dashboard:widget_links'),
|
||||||
('dashboard_zeugmeister', 'dashboard:widget_banner')
|
('dashboard_zeugmeister', 'dashboard:widget_banner')
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- dashboard_chargen — similar to gruppenfuehrer
|
-- ── dashboard_chargen — mid level ──
|
||||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||||
-- Kalender
|
-- Kalender
|
||||||
('dashboard_chargen', 'kalender:access'),
|
('dashboard_chargen', 'kalender:view'),
|
||||||
('dashboard_chargen', 'kalender:view_events'),
|
|
||||||
('dashboard_chargen', 'kalender:view_bookings'),
|
|
||||||
('dashboard_chargen', 'kalender:create_bookings'),
|
('dashboard_chargen', 'kalender:create_bookings'),
|
||||||
|
('dashboard_chargen', 'kalender:cancel_own_bookings'),
|
||||||
('dashboard_chargen', 'kalender:widget_events'),
|
('dashboard_chargen', 'kalender:widget_events'),
|
||||||
('dashboard_chargen', 'kalender:widget_bookings'),
|
('dashboard_chargen', 'kalender:widget_bookings'),
|
||||||
-- Fahrzeuge
|
-- Fahrzeuge
|
||||||
('dashboard_chargen', 'fahrzeuge:access'),
|
|
||||||
('dashboard_chargen', 'fahrzeuge:view'),
|
('dashboard_chargen', 'fahrzeuge:view'),
|
||||||
('dashboard_chargen', 'fahrzeuge:change_status'),
|
('dashboard_chargen', 'fahrzeuge:change_status'),
|
||||||
('dashboard_chargen', 'fahrzeuge:manage_maintenance'),
|
('dashboard_chargen', 'fahrzeuge:manage_maintenance'),
|
||||||
('dashboard_chargen', 'fahrzeuge:widget'),
|
('dashboard_chargen', 'fahrzeuge:widget'),
|
||||||
-- Einsätze
|
-- Einsätze
|
||||||
('dashboard_chargen', 'einsaetze:access'),
|
|
||||||
('dashboard_chargen', 'einsaetze:view'),
|
('dashboard_chargen', 'einsaetze:view'),
|
||||||
('dashboard_chargen', 'einsaetze:create'),
|
('dashboard_chargen', 'einsaetze:create'),
|
||||||
('dashboard_chargen', 'einsaetze:manage_personnel'),
|
('dashboard_chargen', 'einsaetze:manage_personnel'),
|
||||||
-- Ausrüstung
|
-- Ausrüstung
|
||||||
('dashboard_chargen', 'ausruestung:access'),
|
|
||||||
('dashboard_chargen', 'ausruestung:view'),
|
('dashboard_chargen', 'ausruestung:view'),
|
||||||
('dashboard_chargen', 'ausruestung:create'),
|
('dashboard_chargen', 'ausruestung:create'),
|
||||||
('dashboard_chargen', 'ausruestung:manage_maintenance'),
|
('dashboard_chargen', 'ausruestung:manage_maintenance'),
|
||||||
('dashboard_chargen', 'ausruestung:widget'),
|
('dashboard_chargen', 'ausruestung:widget'),
|
||||||
-- Mitglieder
|
-- Mitglieder
|
||||||
('dashboard_chargen', 'mitglieder:access'),
|
('dashboard_chargen', 'mitglieder:view_own'),
|
||||||
('dashboard_chargen', 'mitglieder:view'),
|
('dashboard_chargen', 'mitglieder:view_all'),
|
||||||
-- Atemschutz
|
-- Atemschutz
|
||||||
('dashboard_chargen', 'atemschutz:access'),
|
|
||||||
('dashboard_chargen', 'atemschutz:view'),
|
('dashboard_chargen', 'atemschutz:view'),
|
||||||
('dashboard_chargen', 'atemschutz:create'),
|
('dashboard_chargen', 'atemschutz:create'),
|
||||||
('dashboard_chargen', 'atemschutz:widget'),
|
('dashboard_chargen', 'atemschutz:widget'),
|
||||||
-- Wissen
|
-- Wissen
|
||||||
('dashboard_chargen', 'wissen:access'),
|
('dashboard_chargen', 'wissen:view'),
|
||||||
('dashboard_chargen', 'wissen:widget_recent'),
|
('dashboard_chargen', 'wissen:widget_recent'),
|
||||||
('dashboard_chargen', 'wissen:widget_search'),
|
('dashboard_chargen', 'wissen:widget_search'),
|
||||||
-- Vikunja
|
-- Vikunja
|
||||||
('dashboard_chargen', 'vikunja:access'),
|
|
||||||
('dashboard_chargen', 'vikunja:create_tasks'),
|
('dashboard_chargen', 'vikunja:create_tasks'),
|
||||||
('dashboard_chargen', 'vikunja:widget_tasks'),
|
('dashboard_chargen', 'vikunja:widget_tasks'),
|
||||||
('dashboard_chargen', 'vikunja:widget_quick_add'),
|
('dashboard_chargen', 'vikunja:widget_quick_add'),
|
||||||
-- Nextcloud
|
|
||||||
('dashboard_chargen', 'nextcloud:access'),
|
|
||||||
('dashboard_chargen', 'nextcloud:widget'),
|
|
||||||
-- Dashboard
|
-- Dashboard
|
||||||
('dashboard_chargen', 'dashboard:widget_links'),
|
('dashboard_chargen', 'dashboard:widget_links'),
|
||||||
('dashboard_chargen', 'dashboard:widget_banner')
|
('dashboard_chargen', 'dashboard:widget_banner')
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- dashboard_moderator — event/calendar management + atemschutz view
|
-- ── dashboard_moderator — event/calendar management ──
|
||||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||||
-- Kalender
|
-- Kalender
|
||||||
('dashboard_moderator', 'kalender:access'),
|
('dashboard_moderator', 'kalender:view'),
|
||||||
('dashboard_moderator', 'kalender:view_events'),
|
('dashboard_moderator', 'kalender:create'),
|
||||||
('dashboard_moderator', 'kalender:view_bookings'),
|
('dashboard_moderator', 'kalender:cancel_own_bookings'),
|
||||||
('dashboard_moderator', 'kalender:create_events'),
|
|
||||||
('dashboard_moderator', 'kalender:create_bookings'),
|
('dashboard_moderator', 'kalender:create_bookings'),
|
||||||
('dashboard_moderator', 'kalender:edit_bookings'),
|
('dashboard_moderator', 'kalender:edit_bookings'),
|
||||||
('dashboard_moderator', 'kalender:manage_categories'),
|
('dashboard_moderator', 'kalender:manage_categories'),
|
||||||
@@ -440,123 +391,96 @@ INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
|||||||
('dashboard_moderator', 'kalender:widget_bookings'),
|
('dashboard_moderator', 'kalender:widget_bookings'),
|
||||||
('dashboard_moderator', 'kalender:widget_quick_add'),
|
('dashboard_moderator', 'kalender:widget_quick_add'),
|
||||||
-- Fahrzeuge
|
-- Fahrzeuge
|
||||||
('dashboard_moderator', 'fahrzeuge:access'),
|
|
||||||
('dashboard_moderator', 'fahrzeuge:view'),
|
('dashboard_moderator', 'fahrzeuge:view'),
|
||||||
('dashboard_moderator', 'fahrzeuge:widget'),
|
('dashboard_moderator', 'fahrzeuge:widget'),
|
||||||
-- Einsätze
|
-- Einsätze
|
||||||
('dashboard_moderator', 'einsaetze:access'),
|
|
||||||
('dashboard_moderator', 'einsaetze:view'),
|
('dashboard_moderator', 'einsaetze:view'),
|
||||||
-- Ausrüstung
|
-- Ausrüstung
|
||||||
('dashboard_moderator', 'ausruestung:access'),
|
|
||||||
('dashboard_moderator', 'ausruestung:view'),
|
('dashboard_moderator', 'ausruestung:view'),
|
||||||
('dashboard_moderator', 'ausruestung:widget'),
|
('dashboard_moderator', 'ausruestung:widget'),
|
||||||
-- Mitglieder
|
-- Mitglieder
|
||||||
('dashboard_moderator', 'mitglieder:access'),
|
('dashboard_moderator', 'mitglieder:view_own'),
|
||||||
('dashboard_moderator', 'mitglieder:view'),
|
('dashboard_moderator', 'mitglieder:view_all'),
|
||||||
-- Atemschutz
|
-- Atemschutz
|
||||||
('dashboard_moderator', 'atemschutz:access'),
|
|
||||||
('dashboard_moderator', 'atemschutz:view'),
|
('dashboard_moderator', 'atemschutz:view'),
|
||||||
('dashboard_moderator', 'atemschutz:widget'),
|
('dashboard_moderator', 'atemschutz:widget'),
|
||||||
-- Wissen
|
-- Wissen
|
||||||
('dashboard_moderator', 'wissen:access'),
|
('dashboard_moderator', 'wissen:view'),
|
||||||
('dashboard_moderator', 'wissen:widget_recent'),
|
('dashboard_moderator', 'wissen:widget_recent'),
|
||||||
('dashboard_moderator', 'wissen:widget_search'),
|
('dashboard_moderator', 'wissen:widget_search'),
|
||||||
-- Vikunja
|
-- Vikunja
|
||||||
('dashboard_moderator', 'vikunja:access'),
|
|
||||||
('dashboard_moderator', 'vikunja:create_tasks'),
|
('dashboard_moderator', 'vikunja:create_tasks'),
|
||||||
('dashboard_moderator', 'vikunja:widget_tasks'),
|
('dashboard_moderator', 'vikunja:widget_tasks'),
|
||||||
('dashboard_moderator', 'vikunja:widget_quick_add'),
|
('dashboard_moderator', 'vikunja:widget_quick_add'),
|
||||||
-- Nextcloud
|
|
||||||
('dashboard_moderator', 'nextcloud:access'),
|
|
||||||
('dashboard_moderator', 'nextcloud:widget'),
|
|
||||||
-- Dashboard
|
-- Dashboard
|
||||||
('dashboard_moderator', 'dashboard:widget_links'),
|
('dashboard_moderator', 'dashboard:widget_links'),
|
||||||
('dashboard_moderator', 'dashboard:widget_banner')
|
('dashboard_moderator', 'dashboard:widget_banner')
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- dashboard_atemschutz — atemschutz specialist
|
-- ── dashboard_atemschutz — atemschutz specialist ──
|
||||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||||
-- Kalender (basic access)
|
-- Kalender
|
||||||
('dashboard_atemschutz', 'kalender:access'),
|
('dashboard_atemschutz', 'kalender:view'),
|
||||||
('dashboard_atemschutz', 'kalender:view_events'),
|
|
||||||
('dashboard_atemschutz', 'kalender:view_bookings'),
|
|
||||||
('dashboard_atemschutz', 'kalender:create_bookings'),
|
('dashboard_atemschutz', 'kalender:create_bookings'),
|
||||||
|
('dashboard_atemschutz', 'kalender:cancel_own_bookings'),
|
||||||
('dashboard_atemschutz', 'kalender:widget_events'),
|
('dashboard_atemschutz', 'kalender:widget_events'),
|
||||||
('dashboard_atemschutz', 'kalender:widget_bookings'),
|
('dashboard_atemschutz', 'kalender:widget_bookings'),
|
||||||
-- Fahrzeuge (read)
|
-- Fahrzeuge
|
||||||
('dashboard_atemschutz', 'fahrzeuge:access'),
|
|
||||||
('dashboard_atemschutz', 'fahrzeuge:view'),
|
('dashboard_atemschutz', 'fahrzeuge:view'),
|
||||||
('dashboard_atemschutz', 'fahrzeuge:widget'),
|
('dashboard_atemschutz', 'fahrzeuge:widget'),
|
||||||
-- Einsätze (read)
|
-- Einsätze
|
||||||
('dashboard_atemschutz', 'einsaetze:access'),
|
|
||||||
('dashboard_atemschutz', 'einsaetze:view'),
|
('dashboard_atemschutz', 'einsaetze:view'),
|
||||||
-- Ausrüstung (read)
|
-- Ausrüstung
|
||||||
('dashboard_atemschutz', 'ausruestung:access'),
|
|
||||||
('dashboard_atemschutz', 'ausruestung:view'),
|
('dashboard_atemschutz', 'ausruestung:view'),
|
||||||
('dashboard_atemschutz', 'ausruestung:widget'),
|
('dashboard_atemschutz', 'ausruestung:widget'),
|
||||||
-- Mitglieder (read)
|
-- Mitglieder
|
||||||
('dashboard_atemschutz', 'mitglieder:access'),
|
('dashboard_atemschutz', 'mitglieder:view_own'),
|
||||||
('dashboard_atemschutz', 'mitglieder:view'),
|
('dashboard_atemschutz', 'mitglieder:view_all'),
|
||||||
-- Atemschutz (full)
|
-- Atemschutz
|
||||||
('dashboard_atemschutz', 'atemschutz:access'),
|
|
||||||
('dashboard_atemschutz', 'atemschutz:view'),
|
('dashboard_atemschutz', 'atemschutz:view'),
|
||||||
('dashboard_atemschutz', 'atemschutz:create'),
|
('dashboard_atemschutz', 'atemschutz:create'),
|
||||||
('dashboard_atemschutz', 'atemschutz:widget'),
|
('dashboard_atemschutz', 'atemschutz:widget'),
|
||||||
-- Wissen
|
-- Wissen
|
||||||
('dashboard_atemschutz', 'wissen:access'),
|
('dashboard_atemschutz', 'wissen:view'),
|
||||||
('dashboard_atemschutz', 'wissen:widget_recent'),
|
('dashboard_atemschutz', 'wissen:widget_recent'),
|
||||||
('dashboard_atemschutz', 'wissen:widget_search'),
|
('dashboard_atemschutz', 'wissen:widget_search'),
|
||||||
-- Vikunja
|
-- Vikunja
|
||||||
('dashboard_atemschutz', 'vikunja:access'),
|
|
||||||
('dashboard_atemschutz', 'vikunja:create_tasks'),
|
('dashboard_atemschutz', 'vikunja:create_tasks'),
|
||||||
('dashboard_atemschutz', 'vikunja:widget_tasks'),
|
('dashboard_atemschutz', 'vikunja:widget_tasks'),
|
||||||
('dashboard_atemschutz', 'vikunja:widget_quick_add'),
|
('dashboard_atemschutz', 'vikunja:widget_quick_add'),
|
||||||
-- Nextcloud
|
|
||||||
('dashboard_atemschutz', 'nextcloud:access'),
|
|
||||||
('dashboard_atemschutz', 'nextcloud:widget'),
|
|
||||||
-- Dashboard
|
-- Dashboard
|
||||||
('dashboard_atemschutz', 'dashboard:widget_links'),
|
('dashboard_atemschutz', 'dashboard:widget_links'),
|
||||||
('dashboard_atemschutz', 'dashboard:widget_banner')
|
('dashboard_atemschutz', 'dashboard:widget_banner')
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
-- dashboard_mitglied — basic member access (read + basic booking creation)
|
-- ── dashboard_mitglied — basic member ──
|
||||||
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
INSERT INTO group_permissions (authentik_group, permission_id) VALUES
|
||||||
-- Kalender
|
-- Kalender
|
||||||
('dashboard_mitglied', 'kalender:access'),
|
('dashboard_mitglied', 'kalender:view'),
|
||||||
('dashboard_mitglied', 'kalender:view_events'),
|
|
||||||
('dashboard_mitglied', 'kalender:view_bookings'),
|
|
||||||
('dashboard_mitglied', 'kalender:create_bookings'),
|
('dashboard_mitglied', 'kalender:create_bookings'),
|
||||||
|
('dashboard_mitglied', 'kalender:cancel_own_bookings'),
|
||||||
('dashboard_mitglied', 'kalender:widget_events'),
|
('dashboard_mitglied', 'kalender:widget_events'),
|
||||||
('dashboard_mitglied', 'kalender:widget_bookings'),
|
('dashboard_mitglied', 'kalender:widget_bookings'),
|
||||||
-- Fahrzeuge
|
-- Fahrzeuge
|
||||||
('dashboard_mitglied', 'fahrzeuge:access'),
|
|
||||||
('dashboard_mitglied', 'fahrzeuge:view'),
|
('dashboard_mitglied', 'fahrzeuge:view'),
|
||||||
('dashboard_mitglied', 'fahrzeuge:widget'),
|
('dashboard_mitglied', 'fahrzeuge:widget'),
|
||||||
-- Einsätze
|
-- Einsätze
|
||||||
('dashboard_mitglied', 'einsaetze:access'),
|
|
||||||
('dashboard_mitglied', 'einsaetze:view'),
|
('dashboard_mitglied', 'einsaetze:view'),
|
||||||
-- Ausrüstung
|
-- Ausrüstung
|
||||||
('dashboard_mitglied', 'ausruestung:access'),
|
|
||||||
('dashboard_mitglied', 'ausruestung:view'),
|
('dashboard_mitglied', 'ausruestung:view'),
|
||||||
('dashboard_mitglied', 'ausruestung:widget'),
|
('dashboard_mitglied', 'ausruestung:widget'),
|
||||||
-- Mitglieder
|
-- Mitglieder
|
||||||
('dashboard_mitglied', 'mitglieder:access'),
|
('dashboard_mitglied', 'mitglieder:view_own'),
|
||||||
('dashboard_mitglied', 'mitglieder:view'),
|
|
||||||
-- Atemschutz
|
-- Atemschutz
|
||||||
('dashboard_mitglied', 'atemschutz:access'),
|
|
||||||
('dashboard_mitglied', 'atemschutz:widget'),
|
('dashboard_mitglied', 'atemschutz:widget'),
|
||||||
-- Wissen
|
-- Wissen
|
||||||
('dashboard_mitglied', 'wissen:access'),
|
('dashboard_mitglied', 'wissen:view'),
|
||||||
('dashboard_mitglied', 'wissen:widget_recent'),
|
('dashboard_mitglied', 'wissen:widget_recent'),
|
||||||
('dashboard_mitglied', 'wissen:widget_search'),
|
('dashboard_mitglied', 'wissen:widget_search'),
|
||||||
-- Vikunja
|
-- Vikunja
|
||||||
('dashboard_mitglied', 'vikunja:access'),
|
|
||||||
('dashboard_mitglied', 'vikunja:create_tasks'),
|
('dashboard_mitglied', 'vikunja:create_tasks'),
|
||||||
('dashboard_mitglied', 'vikunja:widget_tasks'),
|
('dashboard_mitglied', 'vikunja:widget_tasks'),
|
||||||
('dashboard_mitglied', 'vikunja:widget_quick_add'),
|
('dashboard_mitglied', 'vikunja:widget_quick_add'),
|
||||||
-- Nextcloud
|
|
||||||
('dashboard_mitglied', 'nextcloud:access'),
|
|
||||||
('dashboard_mitglied', 'nextcloud:widget'),
|
|
||||||
-- Dashboard
|
-- Dashboard
|
||||||
('dashboard_mitglied', 'dashboard:widget_links'),
|
('dashboard_mitglied', 'dashboard:widget_links'),
|
||||||
('dashboard_mitglied', 'dashboard:widget_banner')
|
('dashboard_mitglied', 'dashboard:widget_banner')
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ export type AppRole =
|
|||||||
*
|
*
|
||||||
* Hardwired rules:
|
* Hardwired rules:
|
||||||
* - `dashboard_admin` group always passes (full access).
|
* - `dashboard_admin` group always passes (full access).
|
||||||
* - `admin:access` is checked via group membership (not DB).
|
|
||||||
* - Maintenance mode blocks non-admin access per feature group.
|
* - Maintenance mode blocks non-admin access per feature group.
|
||||||
*/
|
*/
|
||||||
export function requirePermission(permission: string) {
|
export function requirePermission(permission: string) {
|
||||||
@@ -44,25 +43,6 @@ export function requirePermission(permission: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hardwired: admin:access only for dashboard_admin (already returned above)
|
|
||||||
if (permission === 'admin:access') {
|
|
||||||
logger.warn('Permission denied — admin:access', {
|
|
||||||
userId: req.user.id,
|
|
||||||
permission,
|
|
||||||
path: req.path,
|
|
||||||
});
|
|
||||||
|
|
||||||
auditPermissionDenied(req, AuditResourceType.SYSTEM, undefined, {
|
|
||||||
required_permission: permission,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Keine Berechtigung',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check maintenance mode for the feature group
|
// Check maintenance mode for the feature group
|
||||||
const featureGroup = permission.split(':')[0];
|
const featureGroup = permission.split(':')[0];
|
||||||
if (permissionService.isFeatureInMaintenance(featureGroup)) {
|
if (permissionService.isFeatureInMaintenance(featureGroup)) {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ function parseAuditQuery(query: Record<string, unknown>): AuditFilters {
|
|||||||
router.get(
|
router.get(
|
||||||
'/audit-log',
|
'/audit-log',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('admin:access'),
|
requirePermission('admin:view'),
|
||||||
async (req: Request, res: Response): Promise<void> => {
|
async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const filters = parseAuditQuery(req.query as Record<string, unknown>);
|
const filters = parseAuditQuery(req.query as Record<string, unknown>);
|
||||||
@@ -122,7 +122,7 @@ router.get(
|
|||||||
router.get(
|
router.get(
|
||||||
'/audit-log/export',
|
'/audit-log/export',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('admin:access'),
|
requirePermission('admin:view'),
|
||||||
async (req: Request, res: Response): Promise<void> => {
|
async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
// For CSV exports we fetch up to 10,000 rows (no pagination).
|
// For CSV exports we fetch up to 10,000 rows (no pagination).
|
||||||
@@ -176,7 +176,7 @@ const FDISK_SYNC_URL = process.env.FDISK_SYNC_URL ?? '';
|
|||||||
router.get(
|
router.get(
|
||||||
'/fdisk-sync/logs',
|
'/fdisk-sync/logs',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('admin:access'),
|
requirePermission('admin:view'),
|
||||||
async (_req: Request, res: Response): Promise<void> => {
|
async (_req: Request, res: Response): Promise<void> => {
|
||||||
if (!FDISK_SYNC_URL) {
|
if (!FDISK_SYNC_URL) {
|
||||||
res.status(503).json({ success: false, message: 'FDISK sync service not configured' });
|
res.status(503).json({ success: false, message: 'FDISK sync service not configured' });
|
||||||
@@ -194,7 +194,7 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
'/fdisk-sync/trigger',
|
'/fdisk-sync/trigger',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('admin:access'),
|
requirePermission('admin:view'),
|
||||||
async (req: Request, res: Response): Promise<void> => {
|
async (req: Request, res: Response): Promise<void> => {
|
||||||
if (!FDISK_SYNC_URL) {
|
if (!FDISK_SYNC_URL) {
|
||||||
res.status(503).json({ success: false, message: 'FDISK sync service not configured' });
|
res.status(503).json({ success: false, message: 'FDISK sync service not configured' });
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { authenticate } from '../middleware/auth.middleware';
|
|||||||
import { requirePermission } from '../middleware/rbac.middleware';
|
import { requirePermission } from '../middleware/rbac.middleware';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const adminAuth = [authenticate, requirePermission('admin:access')] as const;
|
const adminAuth = [authenticate, requirePermission('admin:write')] as const;
|
||||||
|
|
||||||
// Public (authenticated): get active banners
|
// Public (authenticated): get active banners
|
||||||
router.get('/active', authenticate, bannerController.getActive.bind(bannerController));
|
router.get('/active', authenticate, bannerController.getActive.bind(bannerController));
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
'/import',
|
'/import',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('kalender:create_events'),
|
requirePermission('kalender:create'),
|
||||||
eventsController.importEvents.bind(eventsController)
|
eventsController.importEvents.bind(eventsController)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@ router.post(
|
|||||||
router.post(
|
router.post(
|
||||||
'/',
|
'/',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('kalender:create_events'),
|
requirePermission('kalender:create'),
|
||||||
eventsController.createEvent.bind(eventsController)
|
eventsController.createEvent.bind(eventsController)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ router.get('/:id', authenticate, eventsController.getById.bind(eventsController)
|
|||||||
router.patch(
|
router.patch(
|
||||||
'/:id',
|
'/:id',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('kalender:create_events'),
|
requirePermission('kalender:create'),
|
||||||
eventsController.updateEvent.bind(eventsController)
|
eventsController.updateEvent.bind(eventsController)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ router.patch(
|
|||||||
router.delete(
|
router.delete(
|
||||||
'/:id',
|
'/:id',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('kalender:create_events'),
|
requirePermission('kalender:create'),
|
||||||
eventsController.cancelEvent.bind(eventsController)
|
eventsController.cancelEvent.bind(eventsController)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ router.delete(
|
|||||||
router.post(
|
router.post(
|
||||||
'/:id/delete',
|
'/:id/delete',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('kalender:create_events'),
|
requirePermission('kalender:create'),
|
||||||
eventsController.deleteEvent.bind(eventsController)
|
eventsController.deleteEvent.bind(eventsController)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -17,19 +17,19 @@ router.use(authenticate);
|
|||||||
// "stats" as a userId parameter.
|
// "stats" as a userId parameter.
|
||||||
router.get(
|
router.get(
|
||||||
'/stats',
|
'/stats',
|
||||||
requirePermission('mitglieder:view'),
|
requirePermission('mitglieder:view_all'),
|
||||||
memberController.getMemberStats.bind(memberController)
|
memberController.getMemberStats.bind(memberController)
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/',
|
'/',
|
||||||
requirePermission('mitglieder:view'),
|
requirePermission('mitglieder:view_all'),
|
||||||
memberController.getMembers.bind(memberController)
|
memberController.getMembers.bind(memberController)
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:userId',
|
'/:userId',
|
||||||
requirePermission('mitglieder:view'),
|
requirePermission('mitglieder:view_all'),
|
||||||
memberController.getMemberById.bind(memberController)
|
memberController.getMemberById.bind(memberController)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -41,25 +41,25 @@ router.post(
|
|||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:userId/befoerderungen',
|
'/:userId/befoerderungen',
|
||||||
requirePermission('mitglieder:view'),
|
requirePermission('mitglieder:view_all'),
|
||||||
memberController.getBefoerderungen.bind(memberController)
|
memberController.getBefoerderungen.bind(memberController)
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:userId/untersuchungen',
|
'/:userId/untersuchungen',
|
||||||
requirePermission('mitglieder:view'),
|
requirePermission('mitglieder:view_all'),
|
||||||
memberController.getUntersuchungen.bind(memberController)
|
memberController.getUntersuchungen.bind(memberController)
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:userId/fahrgenehmigungen',
|
'/:userId/fahrgenehmigungen',
|
||||||
requirePermission('mitglieder:view'),
|
requirePermission('mitglieder:view_all'),
|
||||||
memberController.getFahrgenehmigungen.bind(memberController)
|
memberController.getFahrgenehmigungen.bind(memberController)
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:userId/ausbildungen',
|
'/:userId/ausbildungen',
|
||||||
requirePermission('mitglieder:view'),
|
requirePermission('mitglieder:view_all'),
|
||||||
memberController.getAusbildungen.bind(memberController)
|
memberController.getAusbildungen.bind(memberController)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ const router = Router();
|
|||||||
router.get('/me', authenticate, permissionController.getMyPermissions.bind(permissionController));
|
router.get('/me', authenticate, permissionController.getMyPermissions.bind(permissionController));
|
||||||
|
|
||||||
// ── Admin-only routes ─────────────────────────────────────────────────────
|
// ── Admin-only routes ─────────────────────────────────────────────────────
|
||||||
router.get('/admin/matrix', authenticate, requirePermission('admin:access'), permissionController.getMatrix.bind(permissionController));
|
router.get('/admin/matrix', authenticate, requirePermission('admin:view'), permissionController.getMatrix.bind(permissionController));
|
||||||
router.get('/admin/groups', authenticate, requirePermission('admin:access'), permissionController.getGroups.bind(permissionController));
|
router.get('/admin/groups', authenticate, requirePermission('admin:view'), permissionController.getGroups.bind(permissionController));
|
||||||
router.put('/admin/group/:groupName', authenticate, requirePermission('admin:access'), permissionController.setGroupPermissions.bind(permissionController));
|
router.get('/admin/unknown-groups', authenticate, requirePermission('admin:view'), permissionController.getUnknownGroups.bind(permissionController));
|
||||||
router.put('/admin/maintenance/:featureGroupId', authenticate, requirePermission('admin:access'), permissionController.setMaintenanceFlag.bind(permissionController));
|
router.put('/admin/group/:groupName', authenticate, requirePermission('admin:write'), permissionController.setGroupPermissions.bind(permissionController));
|
||||||
|
router.put('/admin/maintenance/:featureGroupId', authenticate, requirePermission('admin:write'), permissionController.setMaintenanceFlag.bind(permissionController));
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { authenticate } from '../middleware/auth.middleware';
|
|||||||
import { requirePermission } from '../middleware/rbac.middleware';
|
import { requirePermission } from '../middleware/rbac.middleware';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const auth = [authenticate, requirePermission('admin:access')] as const;
|
const auth = [authenticate, requirePermission('admin:view')] as const;
|
||||||
|
|
||||||
// Static routes first (before parameterized :id routes)
|
// Static routes first (before parameterized :id routes)
|
||||||
router.get('/services/ping', ...auth, serviceMonitorController.pingAll.bind(serviceMonitorController));
|
router.get('/services/ping', ...auth, serviceMonitorController.pingAll.bind(serviceMonitorController));
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { authenticate } from '../middleware/auth.middleware';
|
|||||||
import { requirePermission } from '../middleware/rbac.middleware';
|
import { requirePermission } from '../middleware/rbac.middleware';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
const auth = [authenticate, requirePermission('admin:access')] as const;
|
const auth = [authenticate, requirePermission('admin:write')] as const;
|
||||||
|
|
||||||
router.get('/', ...auth, settingsController.getAll.bind(settingsController));
|
router.get('/', ...auth, settingsController.getAll.bind(settingsController));
|
||||||
router.get('/preferences', authenticate, settingsController.getUserPreferences.bind(settingsController));
|
router.get('/preferences', authenticate, settingsController.getUserPreferences.bind(settingsController));
|
||||||
|
|||||||
@@ -91,12 +91,12 @@ router.get(
|
|||||||
/**
|
/**
|
||||||
* POST /api/training
|
* POST /api/training
|
||||||
* Create a new training event.
|
* Create a new training event.
|
||||||
* Requires Gruppenführer or above (requirePermission('kalender:create_training')).
|
* Requires Gruppenführer or above (requirePermission('kalender:create')).
|
||||||
*/
|
*/
|
||||||
router.post(
|
router.post(
|
||||||
'/',
|
'/',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('kalender:create_training'),
|
requirePermission('kalender:create'),
|
||||||
trainingController.createEvent
|
trainingController.createEvent
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ router.post(
|
|||||||
router.patch(
|
router.patch(
|
||||||
'/:id',
|
'/:id',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('kalender:create_training'),
|
requirePermission('kalender:create'),
|
||||||
trainingController.updateEvent
|
trainingController.updateEvent
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ router.patch(
|
|||||||
router.delete(
|
router.delete(
|
||||||
'/:id',
|
'/:id',
|
||||||
authenticate,
|
authenticate,
|
||||||
requirePermission('kalender:cancel_training'),
|
requirePermission('kalender:cancel'),
|
||||||
trainingController.cancelEvent
|
trainingController.cancelEvent
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,19 @@ class PermissionService {
|
|||||||
return result.rows.map((r: any) => r.authentik_group);
|
return result.rows.map((r: any) => r.authentik_group);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUnknownGroups(): Promise<string[]> {
|
||||||
|
// Groups from users table that are not yet in the permission matrix
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT DISTINCT g AS group_name
|
||||||
|
FROM users, unnest(authentik_groups) AS g
|
||||||
|
WHERE g LIKE 'dashboard_%'
|
||||||
|
AND g NOT IN (SELECT DISTINCT authentik_group FROM group_permissions)
|
||||||
|
AND g != 'dashboard_admin'
|
||||||
|
ORDER BY group_name
|
||||||
|
`);
|
||||||
|
return result.rows.map((r: any) => r.group_name);
|
||||||
|
}
|
||||||
|
|
||||||
async setGroupPermissions(group: string, permIds: string[], grantedBy: string): Promise<void> {
|
async setGroupPermissions(group: string, permIds: string[], grantedBy: string): Promise<void> {
|
||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
|
Button,
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -19,12 +21,141 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ExpandMore, ExpandLess } from '@mui/icons-material';
|
import { ExpandMore, ExpandLess, Add as AddIcon } from '@mui/icons-material';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { permissionsApi } from '../../services/permissions';
|
import { permissionsApi } from '../../services/permissions';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
import type { PermissionMatrix, FeatureGroup, Permission } from '../../types/permissions.types';
|
import type { PermissionMatrix, FeatureGroup, Permission } from '../../types/permissions.types';
|
||||||
|
|
||||||
|
// ── Permission dependency map ──
|
||||||
|
// Each permission lists its prerequisites (must also be granted).
|
||||||
|
const PERMISSION_DEPS: Record<string, string[]> = {
|
||||||
|
// kalender
|
||||||
|
'kalender:create': ['kalender:view'],
|
||||||
|
'kalender:cancel': ['kalender:view'],
|
||||||
|
'kalender:mark_attendance': ['kalender:view'],
|
||||||
|
'kalender:create_bookings': ['kalender:view'],
|
||||||
|
'kalender:edit_bookings': ['kalender:view', 'kalender:create_bookings'],
|
||||||
|
'kalender:cancel_own_bookings': ['kalender:view'],
|
||||||
|
'kalender:delete_bookings': ['kalender:view', 'kalender:edit_bookings'],
|
||||||
|
'kalender:manage_categories': ['kalender:view'],
|
||||||
|
'kalender:view_reports': ['kalender:view'],
|
||||||
|
'kalender:widget_events': ['kalender:view'],
|
||||||
|
'kalender:widget_bookings': ['kalender:view'],
|
||||||
|
'kalender:widget_quick_add': ['kalender:view', 'kalender:create'],
|
||||||
|
// fahrzeuge
|
||||||
|
'fahrzeuge:create': ['fahrzeuge:view'],
|
||||||
|
'fahrzeuge:change_status': ['fahrzeuge:view'],
|
||||||
|
'fahrzeuge:manage_maintenance': ['fahrzeuge:view'],
|
||||||
|
'fahrzeuge:delete': ['fahrzeuge:view', 'fahrzeuge:create'],
|
||||||
|
'fahrzeuge:widget': ['fahrzeuge:view'],
|
||||||
|
// einsaetze
|
||||||
|
'einsaetze:view_reports': ['einsaetze:view'],
|
||||||
|
'einsaetze:create': ['einsaetze:view'],
|
||||||
|
'einsaetze:delete': ['einsaetze:view', 'einsaetze:create'],
|
||||||
|
'einsaetze:manage_personnel': ['einsaetze:view'],
|
||||||
|
// ausruestung
|
||||||
|
'ausruestung:create': ['ausruestung:view'],
|
||||||
|
'ausruestung:manage_maintenance': ['ausruestung:view'],
|
||||||
|
'ausruestung:delete': ['ausruestung:view', 'ausruestung:create'],
|
||||||
|
'ausruestung:widget': ['ausruestung:view'],
|
||||||
|
// mitglieder
|
||||||
|
'mitglieder:view_all': ['mitglieder:view_own'],
|
||||||
|
'mitglieder:edit': ['mitglieder:view_own', 'mitglieder:view_all'],
|
||||||
|
'mitglieder:create_profile': ['mitglieder:view_own', 'mitglieder:view_all', 'mitglieder:edit'],
|
||||||
|
// atemschutz
|
||||||
|
'atemschutz:create': ['atemschutz:view'],
|
||||||
|
'atemschutz:delete': ['atemschutz:view', 'atemschutz:create'],
|
||||||
|
'atemschutz:widget': ['atemschutz:view'],
|
||||||
|
// wissen
|
||||||
|
'wissen:widget_recent': ['wissen:view'],
|
||||||
|
'wissen:widget_search': ['wissen:view'],
|
||||||
|
// admin
|
||||||
|
'admin:write': ['admin:view'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Group hierarchy ──
|
||||||
|
// When a permission is granted to a group, it is also auto-granted to all listed groups.
|
||||||
|
const 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_atemschutz': ['dashboard_kommando'],
|
||||||
|
'dashboard_moderator': ['dashboard_kommando'],
|
||||||
|
'dashboard_zeugmeister': ['dashboard_kommando'],
|
||||||
|
'dashboard_fahrmeister': ['dashboard_kommando'],
|
||||||
|
'dashboard_gruppenfuehrer': ['dashboard_kommando'],
|
||||||
|
'dashboard_kommando': [],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build reverse hierarchy: for each group, which groups propagate DOWN to it
|
||||||
|
// i.e. if group X lists Y in its hierarchy, then removing from Y should remove from X
|
||||||
|
function buildReverseHierarchy(): Record<string, string[]> {
|
||||||
|
const reverse: Record<string, string[]> = {};
|
||||||
|
for (const [group, inheritors] of Object.entries(GROUP_HIERARCHY)) {
|
||||||
|
for (const inheritor of inheritors) {
|
||||||
|
if (!reverse[inheritor]) reverse[inheritor] = [];
|
||||||
|
reverse[inheritor].push(group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
const REVERSE_HIERARCHY = buildReverseHierarchy();
|
||||||
|
|
||||||
|
// ── Dependency helpers ──
|
||||||
|
|
||||||
|
/** Recursively collect all prerequisite permissions for `permId`. */
|
||||||
|
function collectAllDeps(permId: string, visited = new Set<string>()): Set<string> {
|
||||||
|
if (visited.has(permId)) return visited;
|
||||||
|
visited.add(permId);
|
||||||
|
const deps = PERMISSION_DEPS[permId] || [];
|
||||||
|
for (const dep of deps) {
|
||||||
|
collectAllDeps(dep, visited);
|
||||||
|
}
|
||||||
|
return visited;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a reverse dependency map: for each permission, which permissions depend on it. */
|
||||||
|
function buildReverseDeps(): Record<string, string[]> {
|
||||||
|
const rev: Record<string, string[]> = {};
|
||||||
|
for (const [perm, deps] of Object.entries(PERMISSION_DEPS)) {
|
||||||
|
for (const dep of deps) {
|
||||||
|
if (!rev[dep]) rev[dep] = [];
|
||||||
|
rev[dep].push(perm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const REVERSE_DEPS = buildReverseDeps();
|
||||||
|
|
||||||
|
/** Recursively collect all permissions that depend on `permId`. */
|
||||||
|
function collectAllDependents(permId: string, visited = new Set<string>()): Set<string> {
|
||||||
|
if (visited.has(permId)) return visited;
|
||||||
|
visited.add(permId);
|
||||||
|
const dependents = REVERSE_DEPS[permId] || [];
|
||||||
|
for (const dep of dependents) {
|
||||||
|
collectAllDependents(dep, visited);
|
||||||
|
}
|
||||||
|
return visited;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Add a permission with all its deps to a set. */
|
||||||
|
function addPermWithDeps(current: Set<string>, permId: string): Set<string> {
|
||||||
|
const allNeeded = collectAllDeps(permId);
|
||||||
|
for (const p of allNeeded) current.add(p);
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a permission and all its dependents from a set. */
|
||||||
|
function removePermWithDependents(current: Set<string>, permId: string): Set<string> {
|
||||||
|
const allToRemove = collectAllDependents(permId);
|
||||||
|
for (const p of allToRemove) current.delete(p);
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Component ──
|
||||||
|
|
||||||
function PermissionMatrixTab() {
|
function PermissionMatrixTab() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { showSuccess, showError } = useNotification();
|
const { showSuccess, showError } = useNotification();
|
||||||
@@ -34,14 +165,18 @@ function PermissionMatrixTab() {
|
|||||||
queryFn: permissionsApi.getMatrix,
|
queryFn: permissionsApi.getMatrix,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Track which feature groups are expanded
|
const { data: unknownGroups } = useQuery<string[]>({
|
||||||
|
queryKey: ['admin-unknown-groups'],
|
||||||
|
queryFn: permissionsApi.getUnknownGroups,
|
||||||
|
});
|
||||||
|
|
||||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const toggleGroup = (groupId: string) => {
|
const toggleGroup = (groupId: string) => {
|
||||||
setExpandedGroups(prev => ({ ...prev, [groupId]: !prev[groupId] }));
|
setExpandedGroups(prev => ({ ...prev, [groupId]: !prev[groupId] }));
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Maintenance toggle mutation ──
|
// ── Maintenance toggle ──
|
||||||
const maintenanceMutation = useMutation({
|
const maintenanceMutation = useMutation({
|
||||||
mutationFn: ({ featureGroup, active }: { featureGroup: string; active: boolean }) =>
|
mutationFn: ({ featureGroup, active }: { featureGroup: string; active: boolean }) =>
|
||||||
permissionsApi.setMaintenanceFlag(featureGroup, active),
|
permissionsApi.setMaintenanceFlag(featureGroup, active),
|
||||||
@@ -53,10 +188,10 @@ function PermissionMatrixTab() {
|
|||||||
onError: () => showError('Fehler beim Aktualisieren des Wartungsmodus'),
|
onError: () => showError('Fehler beim Aktualisieren des Wartungsmodus'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Permission toggle mutation (saves full group permissions) ──
|
// ── Permission save (saves full group permissions) ──
|
||||||
const permissionMutation = useMutation({
|
const permissionMutation = useMutation({
|
||||||
mutationFn: ({ group, permissions }: { group: string; permissions: string[] }) =>
|
mutationFn: (updates: { group: string; permissions: string[] }[]) =>
|
||||||
permissionsApi.setGroupPermissions(group, permissions),
|
Promise.all(updates.map(u => permissionsApi.setGroupPermissions(u.group, u.permissions))),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin-permission-matrix'] });
|
queryClient.invalidateQueries({ queryKey: ['admin-permission-matrix'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['my-permissions'] });
|
queryClient.invalidateQueries({ queryKey: ['my-permissions'] });
|
||||||
@@ -65,40 +200,185 @@ function PermissionMatrixTab() {
|
|||||||
onError: () => showError('Fehler beim Speichern der Berechtigungen'),
|
onError: () => showError('Fehler beim Speichern der Berechtigungen'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlePermissionToggle = useCallback(
|
// ── Add unknown group ──
|
||||||
(group: string, permId: string, currentGrants: Record<string, string[]>) => {
|
const addGroupMutation = useMutation({
|
||||||
const current = currentGrants[group] || [];
|
mutationFn: (groupName: string) => permissionsApi.setGroupPermissions(groupName, []),
|
||||||
const newPerms = current.includes(permId)
|
onSuccess: () => {
|
||||||
? current.filter(p => p !== permId)
|
queryClient.invalidateQueries({ queryKey: ['admin-permission-matrix'] });
|
||||||
: [...current, permId];
|
queryClient.invalidateQueries({ queryKey: ['admin-unknown-groups'] });
|
||||||
permissionMutation.mutate({ group, permissions: newPerms });
|
showSuccess('Gruppe hinzugefügt');
|
||||||
},
|
},
|
||||||
[permissionMutation]
|
onError: () => showError('Fehler beim Hinzufügen der Gruppe'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Compute affected groups when toggling a permission ──
|
||||||
|
const computeUpdates = useCallback(
|
||||||
|
(
|
||||||
|
group: string,
|
||||||
|
permId: string,
|
||||||
|
grants: Record<string, string[]>,
|
||||||
|
allGroups: string[],
|
||||||
|
): { group: string; permissions: string[] }[] => {
|
||||||
|
const currentPerms = new Set(grants[group] || []);
|
||||||
|
const isAdding = !currentPerms.has(permId);
|
||||||
|
const updates: { group: string; permissions: string[] }[] = [];
|
||||||
|
|
||||||
|
if (isAdding) {
|
||||||
|
// Add perm + deps to this group
|
||||||
|
const newPerms = new Set(currentPerms);
|
||||||
|
addPermWithDeps(newPerms, permId);
|
||||||
|
updates.push({ group, permissions: Array.from(newPerms) });
|
||||||
|
|
||||||
|
// Hierarchy: also add to all groups that should inherit upward
|
||||||
|
const inheritors = GROUP_HIERARCHY[group] || [];
|
||||||
|
for (const inheritor of inheritors) {
|
||||||
|
if (!allGroups.includes(inheritor) || inheritor === 'dashboard_admin') continue;
|
||||||
|
const inhPerms = new Set(grants[inheritor] || []);
|
||||||
|
const beforeSize = inhPerms.size;
|
||||||
|
addPermWithDeps(inhPerms, permId);
|
||||||
|
if (inhPerms.size !== beforeSize || !inhPerms.has(permId)) {
|
||||||
|
// Only push if something changed
|
||||||
|
inhPerms.add(permId); // ensure the perm itself is there
|
||||||
|
// Re-add deps just in case
|
||||||
|
const allNeeded = collectAllDeps(permId);
|
||||||
|
for (const p of allNeeded) inhPerms.add(p);
|
||||||
|
updates.push({ group: inheritor, permissions: Array.from(inhPerms) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Deduplicate: if a group already in updates, skip
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return updates.filter(u => {
|
||||||
|
if (seen.has(u.group)) return false;
|
||||||
|
seen.add(u.group);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Remove perm + dependents from this group
|
||||||
|
const newPerms = new Set(currentPerms);
|
||||||
|
removePermWithDependents(newPerms, permId);
|
||||||
|
updates.push({ group, permissions: Array.from(newPerms) });
|
||||||
|
|
||||||
|
// Hierarchy: also remove from all groups that are LOWER (reverse hierarchy)
|
||||||
|
// i.e. groups for which this group appears in their hierarchy list
|
||||||
|
const lowerGroups = REVERSE_HIERARCHY[group] || [];
|
||||||
|
for (const lower of lowerGroups) {
|
||||||
|
if (!allGroups.includes(lower) || lower === 'dashboard_admin') continue;
|
||||||
|
const lowerPerms = new Set(grants[lower] || []);
|
||||||
|
const beforeSize = lowerPerms.size;
|
||||||
|
const hadPerm = lowerPerms.has(permId);
|
||||||
|
removePermWithDependents(lowerPerms, permId);
|
||||||
|
if (lowerPerms.size !== beforeSize || hadPerm) {
|
||||||
|
updates.push({ group: lower, permissions: Array.from(lowerPerms) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return updates.filter(u => {
|
||||||
|
if (seen.has(u.group)) return false;
|
||||||
|
seen.add(u.group);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePermissionToggle = useCallback(
|
||||||
|
(group: string, permId: string, grants: Record<string, string[]>, allGroups: string[]) => {
|
||||||
|
const updates = computeUpdates(group, permId, grants, allGroups);
|
||||||
|
if (updates.length > 0) {
|
||||||
|
permissionMutation.mutate(updates);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[computeUpdates, permissionMutation],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectAllForGroup = useCallback(
|
const handleSelectAllForGroup = useCallback(
|
||||||
(
|
(
|
||||||
authentikGroup: string,
|
authentikGroup: string,
|
||||||
featureGroupId: string,
|
featureGroupId: string,
|
||||||
permissions: Permission[],
|
allPermissions: Permission[],
|
||||||
currentGrants: Record<string, string[]>,
|
grants: Record<string, string[]>,
|
||||||
selectAll: boolean
|
allGroups: string[],
|
||||||
|
selectAll: boolean,
|
||||||
) => {
|
) => {
|
||||||
const fgPermIds = permissions
|
const fgPermIds = allPermissions
|
||||||
.filter(p => p.feature_group_id === featureGroupId)
|
.filter(p => p.feature_group_id === featureGroupId)
|
||||||
.map(p => p.id);
|
.map(p => p.id);
|
||||||
const current = currentGrants[authentikGroup] || [];
|
|
||||||
let newPerms: string[];
|
// Build combined updates across all affected groups
|
||||||
if (selectAll) {
|
const allUpdates = new Map<string, Set<string>>();
|
||||||
const permSet = new Set([...current, ...fgPermIds]);
|
|
||||||
newPerms = Array.from(permSet);
|
// Initialize with current grants for potentially affected groups
|
||||||
} else {
|
const initGroup = (g: string) => {
|
||||||
const removeSet = new Set(fgPermIds);
|
if (!allUpdates.has(g)) {
|
||||||
newPerms = current.filter(p => !removeSet.has(p));
|
allUpdates.set(g, new Set(grants[g] || []));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initGroup(authentikGroup);
|
||||||
|
|
||||||
|
if (selectAll) {
|
||||||
|
// Add all feature group perms with deps
|
||||||
|
for (const permId of fgPermIds) {
|
||||||
|
addPermWithDeps(allUpdates.get(authentikGroup)!, permId);
|
||||||
|
// Hierarchy upward
|
||||||
|
const inheritors = GROUP_HIERARCHY[authentikGroup] || [];
|
||||||
|
for (const inheritor of inheritors) {
|
||||||
|
if (!allGroups.includes(inheritor) || inheritor === 'dashboard_admin') continue;
|
||||||
|
initGroup(inheritor);
|
||||||
|
addPermWithDeps(allUpdates.get(inheritor)!, permId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove all feature group perms with dependents
|
||||||
|
for (const permId of fgPermIds) {
|
||||||
|
removePermWithDependents(allUpdates.get(authentikGroup)!, permId);
|
||||||
|
// Hierarchy downward
|
||||||
|
const lowerGroups = REVERSE_HIERARCHY[authentikGroup] || [];
|
||||||
|
for (const lower of lowerGroups) {
|
||||||
|
if (!allGroups.includes(lower) || lower === 'dashboard_admin') continue;
|
||||||
|
initGroup(lower);
|
||||||
|
removePermWithDependents(allUpdates.get(lower)!, permId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build final update list, only groups that actually changed
|
||||||
|
const updates: { group: string; permissions: string[] }[] = [];
|
||||||
|
for (const [g, perms] of allUpdates) {
|
||||||
|
const original = new Set(grants[g] || []);
|
||||||
|
const newArr = Array.from(perms);
|
||||||
|
if (newArr.length !== original.size || newArr.some(p => !original.has(p))) {
|
||||||
|
updates.push({ group: g, permissions: newArr });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length > 0) {
|
||||||
|
permissionMutation.mutate(updates);
|
||||||
}
|
}
|
||||||
permissionMutation.mutate({ group: authentikGroup, permissions: newPerms });
|
|
||||||
},
|
},
|
||||||
[permissionMutation]
|
[permissionMutation],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── All known permission IDs for dependency tooltip ──
|
||||||
|
const allPermissionIds = useMemo(
|
||||||
|
() => (matrix ? new Set(matrix.permissions.map(p => p.id)) : new Set<string>()),
|
||||||
|
[matrix],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getDepTooltip = useCallback(
|
||||||
|
(permId: string): string => {
|
||||||
|
const deps = PERMISSION_DEPS[permId];
|
||||||
|
if (!deps || deps.length === 0) return '';
|
||||||
|
const labels = deps
|
||||||
|
.filter(d => allPermissionIds.has(d))
|
||||||
|
.map(d => {
|
||||||
|
const p = matrix?.permissions.find(p => p.id === d);
|
||||||
|
return p ? p.label : d;
|
||||||
|
});
|
||||||
|
return labels.length > 0 ? `Benötigt: ${labels.join(', ')}` : '';
|
||||||
|
},
|
||||||
|
[allPermissionIds, matrix],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading || !matrix) {
|
if (isLoading || !matrix) {
|
||||||
@@ -114,6 +394,29 @@ function PermissionMatrixTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||||
|
{/* Unknown Groups Alert */}
|
||||||
|
{unknownGroups && unknownGroups.length > 0 && (
|
||||||
|
<Alert severity="warning" sx={{ alignItems: 'center' }}>
|
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}>
|
||||||
|
Folgende Gruppen wurden in der Benutzertabelle gefunden, sind aber noch nicht in der Berechtigungsmatrix:
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||||
|
{unknownGroups.map(g => (
|
||||||
|
<Button
|
||||||
|
key={g}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
startIcon={<AddIcon />}
|
||||||
|
onClick={() => addGroupMutation.mutate(g)}
|
||||||
|
disabled={addGroupMutation.isPending}
|
||||||
|
>
|
||||||
|
{g} hinzufügen
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Section 1: Maintenance Toggles */}
|
{/* Section 1: Maintenance Toggles */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -162,13 +465,22 @@ function PermissionMatrixTab() {
|
|||||||
<Table size="small" stickyHeader>
|
<Table size="small" stickyHeader>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell sx={{ minWidth: 250, fontWeight: 'bold', position: 'sticky', left: 0, zIndex: 3, bgcolor: 'background.paper' }}>
|
<TableCell
|
||||||
|
sx={{
|
||||||
|
minWidth: 250,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
position: 'sticky',
|
||||||
|
left: 0,
|
||||||
|
zIndex: 3,
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
}}
|
||||||
|
>
|
||||||
Berechtigung
|
Berechtigung
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{/* dashboard_admin column */}
|
{/* dashboard_admin column */}
|
||||||
<Tooltip title="Admin hat immer vollen Zugriff" placement="top">
|
<Tooltip title="Admin hat immer vollen Zugriff" placement="top">
|
||||||
<TableCell align="center" sx={{ minWidth: 120, fontWeight: 'bold', opacity: 0.5 }}>
|
<TableCell align="center" sx={{ minWidth: 120, fontWeight: 'bold', opacity: 0.5 }}>
|
||||||
dashboard_admin
|
admin
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{nonAdminGroups.map(g => (
|
{nonAdminGroups.map(g => (
|
||||||
@@ -215,7 +527,7 @@ function PermissionMatrixTab() {
|
|||||||
{/* Per-group: select all / deselect all */}
|
{/* Per-group: select all / deselect all */}
|
||||||
{nonAdminGroups.map(g => {
|
{nonAdminGroups.map(g => {
|
||||||
const groupGrants = grants[g] || [];
|
const groupGrants = grants[g] || [];
|
||||||
const allGranted = fgPerms.every((p: Permission) => groupGrants.includes(p.id));
|
const allGranted = fgPerms.length > 0 && fgPerms.every((p: Permission) => groupGrants.includes(p.id));
|
||||||
const someGranted = fgPerms.some((p: Permission) => groupGrants.includes(p.id));
|
const someGranted = fgPerms.some((p: Permission) => groupGrants.includes(p.id));
|
||||||
return (
|
return (
|
||||||
<TableCell key={g} align="center">
|
<TableCell key={g} align="center">
|
||||||
@@ -223,7 +535,7 @@ function PermissionMatrixTab() {
|
|||||||
checked={allGranted}
|
checked={allGranted}
|
||||||
indeterminate={someGranted && !allGranted}
|
indeterminate={someGranted && !allGranted}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
handleSelectAllForGroup(g, fg.id, permissions, grants, !allGranted)
|
handleSelectAllForGroup(g, fg.id, permissions, grants, groups, !allGranted)
|
||||||
}
|
}
|
||||||
disabled={permissionMutation.isPending}
|
disabled={permissionMutation.isPending}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -239,7 +551,10 @@ function PermissionMatrixTab() {
|
|||||||
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
<Collapse in={isExpanded} timeout="auto" unmountOnExit>
|
||||||
<Table size="small">
|
<Table size="small">
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{fgPerms.map((perm: Permission) => (
|
{fgPerms.map((perm: Permission) => {
|
||||||
|
const depTooltip = getDepTooltip(perm.id);
|
||||||
|
const tooltipText = [perm.description, depTooltip].filter(Boolean).join('\n');
|
||||||
|
return (
|
||||||
<TableRow key={perm.id} hover>
|
<TableRow key={perm.id} hover>
|
||||||
<TableCell
|
<TableCell
|
||||||
sx={{
|
sx={{
|
||||||
@@ -251,7 +566,7 @@ function PermissionMatrixTab() {
|
|||||||
bgcolor: 'background.paper',
|
bgcolor: 'background.paper',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip title={perm.description || ''} placement="right">
|
<Tooltip title={tooltipText || ''} placement="right">
|
||||||
<span>{perm.label}</span>
|
<span>{perm.label}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -265,7 +580,9 @@ function PermissionMatrixTab() {
|
|||||||
<TableCell key={g} align="center" sx={{ minWidth: 120 }}>
|
<TableCell key={g} align="center" sx={{ minWidth: 120 }}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={isGranted}
|
checked={isGranted}
|
||||||
onChange={() => handlePermissionToggle(g, perm.id, grants)}
|
onChange={() =>
|
||||||
|
handlePermissionToggle(g, perm.id, grants, groups)
|
||||||
|
}
|
||||||
disabled={permissionMutation.isPending}
|
disabled={permissionMutation.isPending}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
@@ -273,7 +590,8 @@ function PermissionMatrixTab() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function makeDefaults() {
|
|||||||
|
|
||||||
const EventQuickAddWidget: React.FC = () => {
|
const EventQuickAddWidget: React.FC = () => {
|
||||||
const { hasPermission } = usePermissionContext();
|
const { hasPermission } = usePermissionContext();
|
||||||
const canWrite = hasPermission('kalender:create_events');
|
const canWrite = hasPermission('kalender:create');
|
||||||
|
|
||||||
const defaults = makeDefaults();
|
const defaults = makeDefaults();
|
||||||
const [titel, setTitel] = useState('');
|
const [titel, setTitel] = useState('');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
|
|
||||||
interface WidgetGroupProps {
|
interface WidgetGroupProps {
|
||||||
@@ -7,6 +8,11 @@ interface WidgetGroupProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function WidgetGroup({ title, children, gridColumn }: WidgetGroupProps) {
|
function WidgetGroup({ title, children, gridColumn }: WidgetGroupProps) {
|
||||||
|
// Count non-null children to hide empty groups
|
||||||
|
const validChildren = React.Children.toArray(children).filter(Boolean);
|
||||||
|
|
||||||
|
if (validChildren.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ const baseNavigationItems: NavigationItem[] = [
|
|||||||
icon: <CalendarMonth />,
|
icon: <CalendarMonth />,
|
||||||
path: '/kalender',
|
path: '/kalender',
|
||||||
subItems: kalenderSubItems,
|
subItems: kalenderSubItems,
|
||||||
permission: 'kalender:access',
|
permission: 'kalender:view',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Fahrzeuge',
|
text: 'Fahrzeuge',
|
||||||
@@ -86,25 +86,25 @@ const baseNavigationItems: NavigationItem[] = [
|
|||||||
text: 'Ausrüstung',
|
text: 'Ausrüstung',
|
||||||
icon: <Build />,
|
icon: <Build />,
|
||||||
path: '/ausruestung',
|
path: '/ausruestung',
|
||||||
permission: 'ausruestung:access',
|
permission: 'ausruestung:view',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Mitglieder',
|
text: 'Mitglieder',
|
||||||
icon: <People />,
|
icon: <People />,
|
||||||
path: '/mitglieder',
|
path: '/mitglieder',
|
||||||
permission: 'mitglieder:access',
|
permission: 'mitglieder:view_own',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Atemschutz',
|
text: 'Atemschutz',
|
||||||
icon: <Air />,
|
icon: <Air />,
|
||||||
path: '/atemschutz',
|
path: '/atemschutz',
|
||||||
permission: 'atemschutz:access',
|
permission: 'atemschutz:view',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Wissen',
|
text: 'Wissen',
|
||||||
icon: <MenuBook />,
|
icon: <MenuBook />,
|
||||||
path: '/wissen',
|
path: '/wissen',
|
||||||
permission: 'wissen:access',
|
permission: 'wissen:view',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { sidebarCollapsed, toggleSidebar } = useLayout();
|
const { sidebarCollapsed, toggleSidebar } = useLayout();
|
||||||
const { hasPermission, isAdmin } = usePermissionContext();
|
const { hasPermission } = usePermissionContext();
|
||||||
|
|
||||||
// Fetch vehicle list for dynamic dropdown sub-items
|
// Fetch vehicle list for dynamic dropdown sub-items
|
||||||
const { data: vehicleList } = useQuery({
|
const { data: vehicleList } = useQuery({
|
||||||
@@ -154,13 +154,13 @@ function Sidebar({ mobileOpen, onMobileClose }: SidebarProps) {
|
|||||||
icon: <DirectionsCar />,
|
icon: <DirectionsCar />,
|
||||||
path: '/fahrzeuge',
|
path: '/fahrzeuge',
|
||||||
subItems: vehicleSubItems.length > 0 ? vehicleSubItems : undefined,
|
subItems: vehicleSubItems.length > 0 ? vehicleSubItems : undefined,
|
||||||
permission: 'fahrzeuge:access',
|
permission: 'fahrzeuge:view',
|
||||||
};
|
};
|
||||||
const items = baseNavigationItems
|
const items = baseNavigationItems
|
||||||
.map((item) => item.path === '/fahrzeuge' ? fahrzeugeItem : item)
|
.map((item) => item.path === '/fahrzeuge' ? fahrzeugeItem : item)
|
||||||
.filter((item) => !item.permission || hasPermission(item.permission));
|
.filter((item) => !item.permission || hasPermission(item.permission));
|
||||||
return isAdmin ? [...items, adminItem, adminSettingsItem] : items;
|
return hasPermission('admin:view') ? [...items, adminItem, adminSettingsItem] : items;
|
||||||
}, [isAdmin, vehicleSubItems, hasPermission]);
|
}, [vehicleSubItems, hasPermission]);
|
||||||
|
|
||||||
// Expand state for items with sub-items — auto-expand when route matches
|
// Expand state for items with sub-items — auto-expand when route matches
|
||||||
const [expandedItems, setExpandedItems] = useState<Record<string, boolean>>({});
|
const [expandedItems, setExpandedItems] = useState<Record<string, boolean>>({});
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import BannerManagementTab from '../components/admin/BannerManagementTab';
|
|||||||
import ServiceModeTab from '../components/admin/ServiceModeTab';
|
import ServiceModeTab from '../components/admin/ServiceModeTab';
|
||||||
import FdiskSyncTab from '../components/admin/FdiskSyncTab';
|
import FdiskSyncTab from '../components/admin/FdiskSyncTab';
|
||||||
import PermissionMatrixTab from '../components/admin/PermissionMatrixTab';
|
import PermissionMatrixTab from '../components/admin/PermissionMatrixTab';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { usePermissionContext } from '../contexts/PermissionContext';
|
||||||
|
|
||||||
interface TabPanelProps {
|
interface TabPanelProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -37,11 +37,9 @@ function AdminDashboard() {
|
|||||||
const t = Number(searchParams.get('tab'));
|
const t = Number(searchParams.get('tab'));
|
||||||
if (t >= 0 && t < ADMIN_TAB_COUNT) setTab(t);
|
if (t >= 0 && t < ADMIN_TAB_COUNT) setTab(t);
|
||||||
}, [searchParams]);
|
}, [searchParams]);
|
||||||
const { user } = useAuth();
|
const { hasPermission } = usePermissionContext();
|
||||||
|
|
||||||
const isAdmin = user?.groups?.includes('dashboard_admin') ?? false;
|
if (!hasPermission('admin:view')) {
|
||||||
|
|
||||||
if (!isAdmin) {
|
|
||||||
return <Navigate to="/dashboard" replace />;
|
return <Navigate to="/dashboard" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { WidgetKey } from '../constants/widgets';
|
|||||||
|
|
||||||
function Dashboard() {
|
function Dashboard() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { hasPermission, isAdmin } = usePermissionContext();
|
const { hasPermission } = usePermissionContext();
|
||||||
const [dataLoading, setDataLoading] = useState(true);
|
const [dataLoading, setDataLoading] = useState(true);
|
||||||
|
|
||||||
const { data: preferences } = useQuery({
|
const { data: preferences } = useQuery({
|
||||||
@@ -99,7 +99,7 @@ function Dashboard() {
|
|||||||
|
|
||||||
{/* Status Group */}
|
{/* Status Group */}
|
||||||
<WidgetGroup title="Status" gridColumn="1 / -1">
|
<WidgetGroup title="Status" gridColumn="1 / -1">
|
||||||
{widgetVisible('vehicles') && (
|
{hasPermission('fahrzeuge:widget') && widgetVisible('vehicles') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '300ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '300ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<VehicleDashboardCard />
|
<VehicleDashboardCard />
|
||||||
@@ -107,7 +107,7 @@ function Dashboard() {
|
|||||||
</Fade>
|
</Fade>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{widgetVisible('equipment') && (
|
{hasPermission('ausruestung:widget') && widgetVisible('equipment') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '350ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '350ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<EquipmentDashboardCard />
|
<EquipmentDashboardCard />
|
||||||
@@ -123,7 +123,7 @@ function Dashboard() {
|
|||||||
</Fade>
|
</Fade>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isAdmin && widgetVisible('adminStatus') && (
|
{hasPermission('admin:view') && widgetVisible('adminStatus') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '440ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '440ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<AdminStatusWidget />
|
<AdminStatusWidget />
|
||||||
@@ -134,7 +134,7 @@ function Dashboard() {
|
|||||||
|
|
||||||
{/* Kalender Group */}
|
{/* Kalender Group */}
|
||||||
<WidgetGroup title="Kalender" gridColumn="1 / -1">
|
<WidgetGroup title="Kalender" gridColumn="1 / -1">
|
||||||
{widgetVisible('events') && (
|
{hasPermission('kalender:widget_events') && widgetVisible('events') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '480ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '480ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<UpcomingEventsWidget />
|
<UpcomingEventsWidget />
|
||||||
@@ -142,7 +142,7 @@ function Dashboard() {
|
|||||||
</Fade>
|
</Fade>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{widgetVisible('vehicleBookingList') && (
|
{hasPermission('kalender:widget_bookings') && widgetVisible('vehicleBookingList') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '520ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '520ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<VehicleBookingListWidget />
|
<VehicleBookingListWidget />
|
||||||
@@ -150,7 +150,7 @@ function Dashboard() {
|
|||||||
</Fade>
|
</Fade>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{widgetVisible('vehicleBooking') && (
|
{hasPermission('kalender:create_bookings') && widgetVisible('vehicleBooking') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '560ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '560ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<VehicleBookingQuickAddWidget />
|
<VehicleBookingQuickAddWidget />
|
||||||
@@ -169,7 +169,7 @@ function Dashboard() {
|
|||||||
|
|
||||||
{/* Dienste Group */}
|
{/* Dienste Group */}
|
||||||
<WidgetGroup title="Dienste" gridColumn="1 / -1">
|
<WidgetGroup title="Dienste" gridColumn="1 / -1">
|
||||||
{widgetVisible('bookstackRecent') && (
|
{hasPermission('wissen:widget_recent') && widgetVisible('bookstackRecent') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '600ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '600ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<BookStackRecentWidget />
|
<BookStackRecentWidget />
|
||||||
@@ -177,7 +177,7 @@ function Dashboard() {
|
|||||||
</Fade>
|
</Fade>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{widgetVisible('bookstackSearch') && (
|
{hasPermission('wissen:widget_search') && widgetVisible('bookstackSearch') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '640ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '640ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<BookStackSearchWidget />
|
<BookStackSearchWidget />
|
||||||
@@ -185,7 +185,7 @@ function Dashboard() {
|
|||||||
</Fade>
|
</Fade>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{widgetVisible('vikunjaTasks') && (
|
{hasPermission('vikunja:widget_tasks') && widgetVisible('vikunjaTasks') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '680ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '680ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<VikunjaMyTasksWidget />
|
<VikunjaMyTasksWidget />
|
||||||
@@ -193,7 +193,7 @@ function Dashboard() {
|
|||||||
</Fade>
|
</Fade>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{widgetVisible('vikunjaQuickAdd') && (
|
{hasPermission('vikunja:widget_quick_add') && widgetVisible('vikunjaQuickAdd') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '720ms' }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: '720ms' }}>
|
||||||
<Box>
|
<Box>
|
||||||
<VikunjaQuickAddWidget />
|
<VikunjaQuickAddWidget />
|
||||||
@@ -204,7 +204,7 @@ function Dashboard() {
|
|||||||
|
|
||||||
{/* Information Group */}
|
{/* Information Group */}
|
||||||
<WidgetGroup title="Information" gridColumn="1 / -1">
|
<WidgetGroup title="Information" gridColumn="1 / -1">
|
||||||
{widgetVisible('links') && linkCollections.map((collection, idx) => (
|
{hasPermission('dashboard:widget_links') && widgetVisible('links') && linkCollections.map((collection, idx) => (
|
||||||
<Fade key={collection.id} in={!dataLoading} timeout={600} style={{ transitionDelay: `${760 + idx * 40}ms` }}>
|
<Fade key={collection.id} in={!dataLoading} timeout={600} style={{ transitionDelay: `${760 + idx * 40}ms` }}>
|
||||||
<Box>
|
<Box>
|
||||||
<LinksWidget collection={collection} />
|
<LinksWidget collection={collection} />
|
||||||
@@ -212,11 +212,13 @@ function Dashboard() {
|
|||||||
</Fade>
|
</Fade>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{hasPermission('dashboard:widget_banner') && (
|
||||||
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: `${760 + linkCollections.length * 40}ms` }}>
|
<Fade in={!dataLoading} timeout={600} style={{ transitionDelay: `${760 + linkCollections.length * 40}ms` }}>
|
||||||
<Box>
|
<Box>
|
||||||
<BannerWidget />
|
<BannerWidget />
|
||||||
</Box>
|
</Box>
|
||||||
</Fade>
|
</Fade>
|
||||||
|
)}
|
||||||
</WidgetGroup>
|
</WidgetGroup>
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ function FahrzeugBuchungen() {
|
|||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
const canCreate = hasPermission('kalender:create_bookings');
|
const canCreate = hasPermission('kalender:create_bookings');
|
||||||
const canWrite = hasPermission('kalender:edit_bookings');
|
const canWrite = hasPermission('kalender:edit_bookings');
|
||||||
|
const canCancelOwn = hasPermission('kalender:cancel_own_bookings');
|
||||||
const canChangeBuchungsArt = hasPermission('kalender:manage_categories');
|
const canChangeBuchungsArt = hasPermission('kalender:manage_categories');
|
||||||
|
|
||||||
// ── Week navigation ────────────────────────────────────────────────────────
|
// ── Week navigation ────────────────────────────────────────────────────────
|
||||||
@@ -691,7 +692,7 @@ function FahrzeugBuchungen() {
|
|||||||
Von: {detailBooking.gebucht_von_name}
|
Von: {detailBooking.gebucht_von_name}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{(canWrite || detailBooking.gebucht_von === user?.id) && (
|
{(canWrite || (canCancelOwn && detailBooking.gebucht_von === user?.id)) && (
|
||||||
<Box sx={{ mt: 1.5, display: 'flex', gap: 1 }}>
|
<Box sx={{ mt: 1.5, display: 'flex', gap: 1 }}>
|
||||||
{canWrite && (
|
{canWrite && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1707,7 +1707,7 @@ export default function Kalender() {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
const canWriteEvents = hasPermission('kalender:create_events');
|
const canWriteEvents = hasPermission('kalender:create');
|
||||||
const canWriteBookings = hasPermission('kalender:edit_bookings');
|
const canWriteBookings = hasPermission('kalender:edit_bookings');
|
||||||
const canCreateBookings = hasPermission('kalender:create_bookings');
|
const canCreateBookings = hasPermission('kalender:create_bookings');
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ function Mitglieder() {
|
|||||||
// --- redirect non-privileged users to their own profile ---
|
// --- redirect non-privileged users to their own profile ---
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
if (!hasPermission('mitglieder:edit')) {
|
if (!hasPermission('mitglieder:view_all')) {
|
||||||
navigate(`/mitglieder/${(user as any).id}`, { replace: true });
|
navigate(`/mitglieder/${(user as any).id}`, { replace: true });
|
||||||
}
|
}
|
||||||
}, [user, navigate, hasPermission]);
|
}, [user, navigate, hasPermission]);
|
||||||
|
|||||||
@@ -1074,7 +1074,7 @@ export default function Veranstaltungen() {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||||
|
|
||||||
const canWrite = hasPermission('kalender:create_events');
|
const canWrite = hasPermission('kalender:create');
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const [viewMonth, setViewMonth] = useState({ year: today.getFullYear(), month: today.getMonth() });
|
const [viewMonth, setViewMonth] = useState({ year: today.getFullYear(), month: today.getMonth() });
|
||||||
|
|||||||
@@ -24,4 +24,9 @@ export const permissionsApi = {
|
|||||||
setMaintenanceFlag: async (featureGroup: string, active: boolean): Promise<void> => {
|
setMaintenanceFlag: async (featureGroup: string, active: boolean): Promise<void> => {
|
||||||
await api.put(`/api/permissions/admin/maintenance/${encodeURIComponent(featureGroup)}`, { active });
|
await api.put(`/api/permissions/admin/maintenance/${encodeURIComponent(featureGroup)}`, { active });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getUnknownGroups: async (): Promise<string[]> => {
|
||||||
|
const r = await api.get('/api/permissions/admin/unknown-groups');
|
||||||
|
return r.data.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user