This commit is contained in:
Matthias Hochmeister
2026-03-16 15:26:43 +01:00
parent 023bd7acbb
commit c15d4a50e0
13 changed files with 142 additions and 43 deletions

View File

@@ -2,6 +2,7 @@ import { Request, Response } from 'express';
import { ZodError } from 'zod';
import bookingService from '../services/booking.service';
import vehicleService from '../services/vehicle.service';
import { hasPermission, resolveRequestRole } from '../middleware/rbac.middleware';
import {
CreateBuchungSchema,
UpdateBuchungSchema,
@@ -87,7 +88,7 @@ class BookingController {
*/
async checkAvailability(req: Request, res: Response): Promise<void> {
try {
const { fahrzeugId, from, to } = req.query;
const { fahrzeugId, from, to, excludeId } = req.query;
if (!fahrzeugId || !from || !to) {
res
.status(400)
@@ -114,7 +115,7 @@ class BookingController {
}
const hasConflict = await bookingService.checkConflict(
fahrzeugId as string, beginn, ende
fahrzeugId as string, beginn, ende, excludeId as string | undefined
);
res.json({ success: true, data: { available: !hasConflict } });
} catch (error) {
@@ -204,8 +205,9 @@ class BookingController {
}
/**
* DELETE /api/bookings/:id
* DELETE /api/bookings/:id or PATCH /api/bookings/:id/cancel
* Soft-cancels a booking (sets abgesagt=TRUE).
* Allowed for booking creator or users with bookings:write permission.
*/
async cancel(req: Request, res: Response): Promise<void> {
try {
@@ -214,6 +216,20 @@ class BookingController {
res.status(400).json({ success: false, message: 'Ungültige Buchungs-ID' });
return;
}
// Check ownership: creator can always cancel their own booking
const booking = await bookingService.getById(id);
if (!booking) {
res.status(404).json({ success: false, message: 'Buchung nicht gefunden' });
return;
}
const isOwner = booking.gebucht_von === req.user!.id;
const role = resolveRequestRole(req);
if (!isOwner && !hasPermission(role, 'bookings:write')) {
res.status(403).json({ success: false, message: 'Keine Berechtigung' });
return;
}
const parsed = CancelBuchungSchema.safeParse(req.body);
if (!parsed.success) {
handleZodError(res, parsed.error);

View File

@@ -69,7 +69,7 @@ const PERMISSION_ROLE_MIN: Record<string, AppRole> = {
function roleFromGroups(groups: string[]): AppRole {
if (groups.includes('dashboard_admin')) return 'admin';
if (groups.includes('dashboard_kommando')) return 'kommandant';
if (groups.includes('dashboard_gruppenfuehrer') || groups.includes('dashboard_fahrmeister') || groups.includes('dashboard_zeugmeister')) return 'gruppenfuehrer';
if (groups.includes('dashboard_gruppenfuehrer') || groups.includes('dashboard_fahrmeister') || groups.includes('dashboard_zeugmeister') || groups.includes('dashboard_chargen')) return 'gruppenfuehrer';
return 'mitglied';
}

View File

@@ -43,6 +43,7 @@ export interface FahrzeugBuchungListItem {
beginn: Date;
ende: Date;
abgesagt: boolean;
gebucht_von: string;
gebucht_von_name?: string | null;
}

View File

@@ -21,9 +21,9 @@ router.get('/calendar-token', authenticate, bookingController.getCalendarToken.b
router.post('/', authenticate, bookingController.create.bind(bookingController));
router.patch('/:id', authenticate, requirePermission('bookings:write'), bookingController.update.bind(bookingController));
// Soft-cancel (sets abgesagt=TRUE)
router.delete('/:id', authenticate, requirePermission('bookings:write'), bookingController.cancel.bind(bookingController));
router.patch('/:id/cancel', authenticate, requirePermission('bookings:write'), bookingController.cancel.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));

View File

@@ -27,6 +27,7 @@ function rowToListItem(row: any): FahrzeugBuchungListItem {
beginn: new Date(row.beginn),
ende: new Date(row.ende),
abgesagt: row.abgesagt,
gebucht_von: row.gebucht_von,
gebucht_von_name: row.gebucht_von_name ?? null,
};
}
@@ -77,7 +78,7 @@ class BookingService {
const query = `
SELECT
b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art,
b.beginn, b.ende, b.abgesagt,
b.beginn, b.ende, b.abgesagt, b.gebucht_von,
f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
u.name AS gebucht_von_name
FROM fahrzeug_buchungen b
@@ -102,7 +103,7 @@ class BookingService {
const query = `
SELECT
b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art,
b.beginn, b.ende, b.abgesagt,
b.beginn, b.ende, b.abgesagt, b.gebucht_von,
f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
u.name AS gebucht_von_name
FROM fahrzeug_buchungen b

View File

@@ -586,6 +586,7 @@ class EventsService {
'dashboard_jugend': 'Feuerwehrjugend',
'dashboard_kommandant': 'Kommandanten',
'dashboard_moderator': 'Moderatoren',
'dashboard_chargen': 'Gruppenkommandanten',
'feuerwehr-admin': 'Feuerwehr Admin',
'feuerwehr-kommandant': 'Feuerwehr Kommandant',
};