rights system

This commit is contained in:
Matthias Hochmeister
2026-03-23 12:00:09 +01:00
parent d173c8235e
commit a575b61d26
5 changed files with 476 additions and 311 deletions

View File

@@ -154,6 +154,52 @@ class PermissionController {
res.status(500).json({ success: false, message: 'Fehler beim Setzen des Wartungsmodus' });
}
}
/**
* GET /api/admin/permissions/config
* Returns the dependency configuration (group hierarchy + permission deps).
*/
async getDependencyConfig(_req: Request, res: Response): Promise<void> {
try {
const config = await permissionService.getDependencyConfig();
res.json({ success: true, data: config });
} catch (error) {
logger.error('Failed to get dependency config', { error });
res.status(500).json({ success: false, message: 'Fehler beim Laden der Konfiguration' });
}
}
/**
* PUT /api/admin/permissions/config
* Updates the dependency configuration.
* Body: { groupHierarchy?: Record<string, string[]>, permissionDeps?: Record<string, string[]> }
*/
async setDependencyConfig(req: Request, res: Response): Promise<void> {
try {
const { groupHierarchy, permissionDeps } = req.body;
if (groupHierarchy !== undefined) {
if (typeof groupHierarchy !== 'object' || groupHierarchy === null) {
res.status(400).json({ success: false, message: 'groupHierarchy must be an object' });
return;
}
await permissionService.setGroupHierarchy(groupHierarchy, req.user!.id);
}
if (permissionDeps !== undefined) {
if (typeof permissionDeps !== 'object' || permissionDeps === null) {
res.status(400).json({ success: false, message: 'permissionDeps must be an object' });
return;
}
await permissionService.setPermissionDeps(permissionDeps, req.user!.id);
}
res.json({ success: true, message: 'Konfiguration aktualisiert' });
} catch (error) {
logger.error('Failed to set dependency config', { error });
res.status(500).json({ success: false, message: 'Fehler beim Speichern der Konfiguration' });
}
}
}
export default new PermissionController();

View File

@@ -12,6 +12,8 @@ router.get('/me', authenticate, permissionController.getMyPermissions.bind(permi
router.get('/admin/matrix', authenticate, requirePermission('admin:view'), permissionController.getMatrix.bind(permissionController));
router.get('/admin/groups', authenticate, requirePermission('admin:view'), permissionController.getGroups.bind(permissionController));
router.get('/admin/unknown-groups', authenticate, requirePermission('admin:view'), permissionController.getUnknownGroups.bind(permissionController));
router.get('/admin/config', authenticate, requirePermission('admin:view'), permissionController.getDependencyConfig.bind(permissionController));
router.put('/admin/config', authenticate, requirePermission('admin:write'), permissionController.setDependencyConfig.bind(permissionController));
router.put('/admin/group/:groupName', authenticate, requirePermission('admin:write'), permissionController.setGroupPermissions.bind(permissionController));
router.put('/admin/bulk', authenticate, requirePermission('admin:write'), permissionController.setBulkPermissions.bind(permissionController));
router.put('/admin/maintenance/:featureGroupId', authenticate, requirePermission('admin:write'), permissionController.setMaintenanceFlag.bind(permissionController));

View File

@@ -1,6 +1,55 @@
import pool from '../config/database';
import logger from '../utils/logger';
// Default configs — used when no DB config exists yet
const DEFAULT_GROUP_HIERARCHY: Record<string, string[]> = {
'dashboard_mitglied': ['dashboard_chargen', 'dashboard_atemschutz', 'dashboard_moderator', 'dashboard_zeugmeister', 'dashboard_fahrmeister', 'dashboard_gruppenfuehrer', 'dashboard_kommando'],
'dashboard_chargen': ['dashboard_gruppenfuehrer', 'dashboard_kommando'],
'dashboard_atemschutz': ['dashboard_kommando'],
'dashboard_moderator': ['dashboard_kommando'],
'dashboard_zeugmeister': ['dashboard_kommando'],
'dashboard_fahrmeister': ['dashboard_kommando'],
'dashboard_gruppenfuehrer': ['dashboard_kommando'],
'dashboard_kommando': [],
};
const DEFAULT_PERMISSION_DEPS: Record<string, string[]> = {
'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:create': ['fahrzeuge:view'],
'fahrzeuge:change_status': ['fahrzeuge:view'],
'fahrzeuge:manage_maintenance': ['fahrzeuge:view'],
'fahrzeuge:delete': ['fahrzeuge:view', 'fahrzeuge:create'],
'fahrzeuge:widget': ['fahrzeuge:view'],
'einsaetze:view_reports': ['einsaetze:view'],
'einsaetze:create': ['einsaetze:view'],
'einsaetze:delete': ['einsaetze:view', 'einsaetze:create'],
'einsaetze:manage_personnel': ['einsaetze:view'],
'ausruestung:create': ['ausruestung:view'],
'ausruestung:manage_maintenance': ['ausruestung:view'],
'ausruestung:delete': ['ausruestung:view', 'ausruestung:create'],
'ausruestung:widget': ['ausruestung:view'],
'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:create': ['atemschutz:view'],
'atemschutz:delete': ['atemschutz:view', 'atemschutz:create'],
'atemschutz:widget': ['atemschutz:view'],
'wissen:widget_recent': ['wissen:view'],
'wissen:widget_search': ['wissen:view'],
'admin:write': ['admin:view'],
};
export interface FeatureGroupRow {
id: string;
label: string;
@@ -255,10 +304,76 @@ class PermissionService {
'UPDATE feature_groups SET maintenance = $1 WHERE id = $2',
[active, featureGroup]
);
// Reload cache
await this.loadCache();
logger.info('Maintenance flag updated', { featureGroup, active });
}
// ── Dependency config (stored in app_settings) ──
async getGroupHierarchy(): Promise<Record<string, string[]>> {
try {
const result = await pool.query(
"SELECT value FROM app_settings WHERE key = 'permission_group_hierarchy'"
);
if (result.rows.length > 0 && result.rows[0].value) {
const val = typeof result.rows[0].value === 'string'
? JSON.parse(result.rows[0].value)
: result.rows[0].value;
return val;
}
} catch (error) {
logger.warn('Failed to load group hierarchy from DB, using defaults', { error });
}
return DEFAULT_GROUP_HIERARCHY;
}
async setGroupHierarchy(hierarchy: Record<string, string[]>, userId: string): Promise<void> {
await pool.query(
`INSERT INTO app_settings (key, value, updated_by, updated_at)
VALUES ('permission_group_hierarchy', $1, $2, NOW())
ON CONFLICT (key) DO UPDATE SET value = $1, updated_by = $2, updated_at = NOW()`,
[JSON.stringify(hierarchy), userId]
);
logger.info('Group hierarchy updated', { userId });
}
async getPermissionDeps(): Promise<Record<string, string[]>> {
try {
const result = await pool.query(
"SELECT value FROM app_settings WHERE key = 'permission_deps'"
);
if (result.rows.length > 0 && result.rows[0].value) {
const val = typeof result.rows[0].value === 'string'
? JSON.parse(result.rows[0].value)
: result.rows[0].value;
return val;
}
} catch (error) {
logger.warn('Failed to load permission deps from DB, using defaults', { error });
}
return DEFAULT_PERMISSION_DEPS;
}
async setPermissionDeps(deps: Record<string, string[]>, userId: string): Promise<void> {
await pool.query(
`INSERT INTO app_settings (key, value, updated_by, updated_at)
VALUES ('permission_deps', $1, $2, NOW())
ON CONFLICT (key) DO UPDATE SET value = $1, updated_by = $2, updated_at = NOW()`,
[JSON.stringify(deps), userId]
);
logger.info('Permission deps updated', { userId });
}
async getDependencyConfig(): Promise<{
groupHierarchy: Record<string, string[]>;
permissionDeps: Record<string, string[]>;
}> {
const [groupHierarchy, permissionDeps] = await Promise.all([
this.getGroupHierarchy(),
this.getPermissionDeps(),
]);
return { groupHierarchy, permissionDeps };
}
}
export const permissionService = new PermissionService();