rights system

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

View File

@@ -16,8 +16,8 @@ router.get('/:id', authenticate, atemschutzController.getOne.bind(atem
// ── Write — gruppenfuehrer+ ─────────────────────────────────────────────────
router.post('/', authenticate, requirePermission('atemschutz:write'), atemschutzController.create.bind(atemschutzController));
router.patch('/:id', authenticate, requirePermission('atemschutz:write'), atemschutzController.update.bind(atemschutzController));
router.post('/', authenticate, requirePermission('atemschutz:create'), atemschutzController.create.bind(atemschutzController));
router.patch('/:id', authenticate, requirePermission('atemschutz:create'), atemschutzController.update.bind(atemschutzController));
// ── Delete — kommandant+ ────────────────────────────────────────────────────

View File

@@ -19,14 +19,14 @@ router.get('/calendar-token', authenticate, bookingController.getCalendarToken.b
// ── Write operations ──────────────────────────────────────────────────────────
router.post('/', authenticate, bookingController.create.bind(bookingController));
router.patch('/:id', authenticate, requirePermission('bookings:write'), bookingController.update.bind(bookingController));
router.patch('/:id', authenticate, requirePermission('kalender:edit_bookings'), bookingController.update.bind(bookingController));
// Soft-cancel (sets abgesagt=TRUE) — creator or bookings:write
router.delete('/:id', authenticate, bookingController.cancel.bind(bookingController));
router.patch('/:id/cancel', authenticate, bookingController.cancel.bind(bookingController));
// Hard-delete (admin only)
router.delete('/:id/force', authenticate, requirePermission('bookings:delete'), bookingController.hardDelete.bind(bookingController));
router.delete('/:id/force', authenticate, requirePermission('kalender:delete_bookings'), bookingController.hardDelete.bind(bookingController));
// ── Single booking read — after specific routes to avoid path conflicts ───────

View File

@@ -17,13 +17,13 @@ router.get('/:id', authenticate, equipmentController.getEquipmen
// ── Write — gruppenfuehrer+ ────────────────────────────────────────────────
router.post('/', authenticate, requirePermission('equipment:write'), equipmentController.createEquipment.bind(equipmentController));
router.patch('/:id', authenticate, requirePermission('equipment:write'), equipmentController.updateEquipment.bind(equipmentController));
router.patch('/:id/status', authenticate, requirePermission('equipment:write'), equipmentController.updateStatus.bind(equipmentController));
router.post('/:id/wartung', authenticate, requirePermission('equipment:write'), equipmentController.addWartung.bind(equipmentController));
router.post('/', authenticate, requirePermission('ausruestung:create'), equipmentController.createEquipment.bind(equipmentController));
router.patch('/:id', authenticate, requirePermission('ausruestung:create'), equipmentController.updateEquipment.bind(equipmentController));
router.patch('/:id/status', authenticate, requirePermission('ausruestung:create'), equipmentController.updateStatus.bind(equipmentController));
router.post('/:id/wartung', authenticate, requirePermission('ausruestung:create'), equipmentController.addWartung.bind(equipmentController));
// ── Delete — admin only ──────────────────────────────────────────────────────
router.delete('/:id', authenticate, requirePermission('equipment:delete'), equipmentController.deleteEquipment.bind(equipmentController));
router.delete('/:id', authenticate, requirePermission('ausruestung:delete'), equipmentController.deleteEquipment.bind(equipmentController));
export default router;

View File

@@ -22,7 +22,7 @@ router.get('/kategorien', authenticate, eventsController.listKategorien.bind(eve
router.post(
'/kategorien',
authenticate,
requirePermission('events:categories'),
requirePermission('kalender:manage_categories'),
eventsController.createKategorie.bind(eventsController)
);
@@ -33,7 +33,7 @@ router.post(
router.patch(
'/kategorien/:id',
authenticate,
requirePermission('events:categories'),
requirePermission('kalender:manage_categories'),
eventsController.updateKategorie.bind(eventsController)
);
@@ -44,7 +44,7 @@ router.patch(
router.delete(
'/kategorien/:id',
authenticate,
requirePermission('events:categories'),
requirePermission('kalender:manage_categories'),
eventsController.deleteKategorie.bind(eventsController)
);
@@ -118,7 +118,7 @@ router.get(
router.post(
'/import',
authenticate,
requirePermission('events:write'),
requirePermission('kalender:create_events'),
eventsController.importEvents.bind(eventsController)
);
@@ -129,7 +129,7 @@ router.post(
router.post(
'/',
authenticate,
requirePermission('events:write'),
requirePermission('kalender:create_events'),
eventsController.createEvent.bind(eventsController)
);
@@ -146,7 +146,7 @@ router.get('/:id', authenticate, eventsController.getById.bind(eventsController)
router.patch(
'/:id',
authenticate,
requirePermission('events:write'),
requirePermission('kalender:create_events'),
eventsController.updateEvent.bind(eventsController)
);
@@ -157,7 +157,7 @@ router.patch(
router.delete(
'/:id',
authenticate,
requirePermission('events:write'),
requirePermission('kalender:create_events'),
eventsController.cancelEvent.bind(eventsController)
);
@@ -168,7 +168,7 @@ router.delete(
router.post(
'/:id/delete',
authenticate,
requirePermission('events:write'),
requirePermission('kalender:create_events'),
eventsController.deleteEvent.bind(eventsController)
);

View File

@@ -16,7 +16,7 @@ router.use(authenticate);
*/
router.get(
'/',
requirePermission('incidents:read'),
requirePermission('einsaetze:view'),
incidentController.listIncidents.bind(incidentController)
);
@@ -31,7 +31,7 @@ router.get(
*/
router.get(
'/stats',
requirePermission('incidents:read'),
requirePermission('einsaetze:view'),
incidentController.getStats.bind(incidentController)
);
@@ -42,7 +42,7 @@ router.get(
*/
router.post(
'/refresh-stats',
requirePermission('incidents:delete'), // kommandant+ (repurposing delete permission for admin ops)
requirePermission('einsaetze:delete'),
incidentController.refreshStats.bind(incidentController)
);
@@ -53,7 +53,7 @@ router.post(
*/
router.get(
'/:id',
requirePermission('incidents:read'),
requirePermission('einsaetze:view'),
incidentController.getIncident.bind(incidentController)
);
@@ -64,7 +64,7 @@ router.get(
*/
router.post(
'/',
requirePermission('incidents:write'),
requirePermission('einsaetze:create'),
incidentController.createIncident.bind(incidentController)
);
@@ -75,7 +75,7 @@ router.post(
*/
router.patch(
'/:id',
requirePermission('incidents:write'),
requirePermission('einsaetze:create'),
incidentController.updateIncident.bind(incidentController)
);
@@ -86,7 +86,7 @@ router.patch(
*/
router.delete(
'/:id',
requirePermission('incidents:delete'),
requirePermission('einsaetze:delete'),
incidentController.deleteIncident.bind(incidentController)
);
@@ -98,7 +98,7 @@ router.delete(
*/
router.post(
'/:id/personnel',
requirePermission('incidents:manage_personnel'),
requirePermission('einsaetze:manage_personnel'),
incidentController.assignPersonnel.bind(incidentController)
);
@@ -109,7 +109,7 @@ router.post(
*/
router.delete(
'/:id/personnel/:userId',
requirePermission('incidents:manage_personnel'),
requirePermission('einsaetze:manage_personnel'),
incidentController.removePersonnel.bind(incidentController)
);
@@ -121,7 +121,7 @@ router.delete(
*/
router.post(
'/:id/vehicles',
requirePermission('incidents:manage_personnel'),
requirePermission('einsaetze:manage_personnel'),
incidentController.assignVehicle.bind(incidentController)
);
@@ -132,7 +132,7 @@ router.post(
*/
router.delete(
'/:id/vehicles/:fahrzeugId',
requirePermission('incidents:manage_personnel'),
requirePermission('einsaetze:manage_personnel'),
incidentController.removeVehicle.bind(incidentController)
);

View File

@@ -17,49 +17,49 @@ router.use(authenticate);
// "stats" as a userId parameter.
router.get(
'/stats',
requirePermission('members:read'),
requirePermission('mitglieder:view'),
memberController.getMemberStats.bind(memberController)
);
router.get(
'/',
requirePermission('members:read'),
requirePermission('mitglieder:view'),
memberController.getMembers.bind(memberController)
);
router.get(
'/:userId',
requirePermission('members:read'),
requirePermission('mitglieder:view'),
memberController.getMemberById.bind(memberController)
);
router.post(
'/:userId/profile',
requirePermission('members:write'),
requirePermission('mitglieder:edit'),
memberController.createMemberProfile.bind(memberController)
);
router.get(
'/:userId/befoerderungen',
requirePermission('members:read'),
requirePermission('mitglieder:view'),
memberController.getBefoerderungen.bind(memberController)
);
router.get(
'/:userId/untersuchungen',
requirePermission('members:read'),
requirePermission('mitglieder:view'),
memberController.getUntersuchungen.bind(memberController)
);
router.get(
'/:userId/fahrgenehmigungen',
requirePermission('members:read'),
requirePermission('mitglieder:view'),
memberController.getFahrgenehmigungen.bind(memberController)
);
router.get(
'/:userId/ausbildungen',
requirePermission('members:read'),
requirePermission('mitglieder:view'),
memberController.getAusbildungen.bind(memberController)
);
@@ -76,7 +76,7 @@ const requireOwnerOrWrite = (req: Request, res: Response, next: NextFunction): v
return;
}
// Not the owner — must have members:write permission
requirePermission('members:write')(req, res, next);
requirePermission('mitglieder:edit')(req, res, next);
};
/**

View File

@@ -0,0 +1,17 @@
import { Router } from 'express';
import permissionController from '../controllers/permission.controller';
import { authenticate } from '../middleware/auth.middleware';
import { requirePermission } from '../middleware/rbac.middleware';
const router = Router();
// ── User-facing (any authenticated user) ──────────────────────────────────
router.get('/me', authenticate, permissionController.getMyPermissions.bind(permissionController));
// ── Admin-only routes ─────────────────────────────────────────────────────
router.get('/admin/matrix', authenticate, requirePermission('admin:access'), permissionController.getMatrix.bind(permissionController));
router.get('/admin/groups', authenticate, requirePermission('admin:access'), permissionController.getGroups.bind(permissionController));
router.put('/admin/group/:groupName', authenticate, requirePermission('admin:access'), permissionController.setGroupPermissions.bind(permissionController));
router.put('/admin/maintenance/:featureGroupId', authenticate, requirePermission('admin:access'), permissionController.setMaintenanceFlag.bind(permissionController));
export default router;

View File

@@ -1,14 +1,15 @@
import { Router, Request, Response, NextFunction } from 'express';
import trainingController from '../controllers/training.controller';
import { authenticate, optionalAuth } from '../middleware/auth.middleware';
import { requirePermission, getUserRole } from '../middleware/rbac.middleware';
import { requirePermission } from '../middleware/rbac.middleware';
import { permissionService } from '../services/permission.service';
const router = Router();
// ---------------------------------------------------------------------------
// injectTeilnahmenFlag
//
// Sets req.canSeeTeilnahmen = true for Gruppenführer and above.
// Sets req.canSeeTeilnahmen = true for users with kalender:mark_attendance.
// Regular Mitglieder see only attendance counts; officers see the full list.
// ---------------------------------------------------------------------------
@@ -19,12 +20,10 @@ async function injectTeilnahmenFlag(
): Promise<void> {
try {
if (req.user) {
const role = await getUserRole(req.user.id);
const ROLE_ORDER: Record<string, number> = {
bewerber: -1, mitglied: 0, gruppenfuehrer: 1, kommandant: 2, admin: 3,
};
const groups: string[] = req.user?.groups ?? [];
(req as any).canSeeTeilnahmen =
(ROLE_ORDER[role] ?? 0) >= ROLE_ORDER.gruppenfuehrer;
groups.includes('dashboard_admin') ||
permissionService.hasPermission(groups, 'kalender:mark_attendance');
}
} catch (_err) {
// Non-fatal — default to restricted view
@@ -68,12 +67,12 @@ router.get('/calendar-token', authenticate, trainingController.getCalendarToken)
/**
* GET /api/training/stats?year=<YYYY>
* Annual participation stats.
* Requires Kommandant or above (requirePermission('reports:read')).
* Requires Kommandant or above (requirePermission('kalender:view_reports')).
*/
router.get(
'/stats',
authenticate,
requirePermission('reports:read'),
requirePermission('kalender:view_reports'),
trainingController.getStats
);
@@ -92,12 +91,12 @@ router.get(
/**
* POST /api/training
* Create a new training event.
* Requires Gruppenführer or above (requirePermission('training:write')).
* Requires Gruppenführer or above (requirePermission('kalender:create_training')).
*/
router.post(
'/',
authenticate,
requirePermission('training:write'),
requirePermission('kalender:create_training'),
trainingController.createEvent
);
@@ -109,7 +108,7 @@ router.post(
router.patch(
'/:id',
authenticate,
requirePermission('training:write'),
requirePermission('kalender:create_training'),
trainingController.updateEvent
);
@@ -121,7 +120,7 @@ router.patch(
router.delete(
'/:id',
authenticate,
requirePermission('training:cancel'),
requirePermission('kalender:cancel_training'),
trainingController.cancelEvent
);
@@ -142,7 +141,7 @@ router.patch(
router.post(
'/:id/attendance/mark',
authenticate,
requirePermission('training:mark_attendance'),
requirePermission('kalender:mark_attendance'),
trainingController.markAttendance
);

View File

@@ -10,19 +10,19 @@ const router = Router();
router.get('/', authenticate, vehicleController.listVehicles.bind(vehicleController));
router.get('/stats', authenticate, vehicleController.getStats.bind(vehicleController));
router.get('/alerts', authenticate, vehicleController.getAlerts.bind(vehicleController));
router.get('/alerts/export', authenticate, requirePermission('vehicles:read'), vehicleController.exportAlerts.bind(vehicleController));
router.get('/alerts/export', authenticate, requirePermission('fahrzeuge:view'), vehicleController.exportAlerts.bind(vehicleController));
router.get('/:id', authenticate, vehicleController.getVehicle.bind(vehicleController));
router.get('/:id/wartung', authenticate, vehicleController.getWartung.bind(vehicleController));
// ── Write — kommandant+ ──────────────────────────────────────────────────────
router.post('/', authenticate, requirePermission('vehicles:write'), vehicleController.createVehicle.bind(vehicleController));
router.patch('/:id', authenticate, requirePermission('vehicles:write'), vehicleController.updateVehicle.bind(vehicleController));
router.delete('/:id', authenticate, requirePermission('vehicles:delete'), vehicleController.deleteVehicle.bind(vehicleController));
router.post('/', authenticate, requirePermission('fahrzeuge:create'), vehicleController.createVehicle.bind(vehicleController));
router.patch('/:id', authenticate, requirePermission('fahrzeuge:create'), vehicleController.updateVehicle.bind(vehicleController));
router.delete('/:id', authenticate, requirePermission('fahrzeuge:delete'), vehicleController.deleteVehicle.bind(vehicleController));
// ── Status + maintenance log — gruppenfuehrer+ ──────────────────────────────
router.patch('/:id/status', authenticate, requirePermission('vehicles:status'), vehicleController.updateVehicleStatus.bind(vehicleController));
router.post('/:id/wartung', authenticate, requirePermission('vehicles:status'), vehicleController.addWartung.bind(vehicleController));
router.patch('/:id/status', authenticate, requirePermission('fahrzeuge:change_status'), vehicleController.updateVehicleStatus.bind(vehicleController));
router.post('/:id/wartung', authenticate, requirePermission('fahrzeuge:change_status'), vehicleController.addWartung.bind(vehicleController));
export default router;