bug fix for atemschutz

This commit is contained in:
Matthias Hochmeister
2026-03-01 19:19:12 +01:00
parent 2630224edd
commit 6495ca94d1
17 changed files with 5116 additions and 0 deletions

View File

@@ -0,0 +1,321 @@
import { Request, Response } from 'express';
import eventsService from '../services/events.service';
import {
CreateKategorieSchema,
UpdateKategorieSchema,
CreateVeranstaltungSchema,
UpdateVeranstaltungSchema,
CancelVeranstaltungSchema,
} from '../models/events.model';
import logger from '../utils/logger';
// ---------------------------------------------------------------------------
// Known Authentik groups exposed to the frontend for event targeting
// ---------------------------------------------------------------------------
const KNOWN_GROUPS = [
{ id: 'dashboard_admin', label: 'Administratoren' },
{ id: 'dashboard_moderator', label: 'Moderatoren' },
{ id: 'dashboard_mitglied', label: 'Mitglieder' },
{ id: 'dashboard_fahrmeister', label: 'Fahrmeister' },
{ id: 'dashboard_zeugmeister', label: 'Zeugmeister' },
{ id: 'dashboard_atemschutz', label: 'Atemschutzwart' },
{ id: 'dashboard_jugend', label: 'Feuerwehrjugend' },
{ id: 'dashboard_kommandant', label: 'Kommandanten' },
];
// ---------------------------------------------------------------------------
// Helper — extract userGroups from request
// ---------------------------------------------------------------------------
function getUserGroups(req: Request): string[] {
return (req.user as any)?.groups ?? [];
}
// ---------------------------------------------------------------------------
// Controller
// ---------------------------------------------------------------------------
class EventsController {
// -------------------------------------------------------------------------
// GET /api/events/kategorien
// -------------------------------------------------------------------------
listKategorien = async (_req: Request, res: Response): Promise<void> => {
try {
const data = await eventsService.getKategorien();
res.json({ success: true, data });
} catch (error) {
logger.error('listKategorien error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Laden der Kategorien' });
}
};
// -------------------------------------------------------------------------
// POST /api/events/kategorien
// -------------------------------------------------------------------------
createKategorie = async (req: Request, res: Response): Promise<void> => {
try {
const parsed = CreateKategorieSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
success: false,
message: 'Validierungsfehler',
errors: parsed.error.flatten().fieldErrors,
});
return;
}
const data = await eventsService.createKategorie(parsed.data, req.user!.id);
res.status(201).json({ success: true, data });
} catch (error) {
logger.error('createKategorie error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Erstellen der Kategorie' });
}
};
// -------------------------------------------------------------------------
// PATCH /api/events/kategorien/:id
// -------------------------------------------------------------------------
updateKategorie = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params as Record<string, string>;
const parsed = UpdateKategorieSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
success: false,
message: 'Validierungsfehler',
errors: parsed.error.flatten().fieldErrors,
});
return;
}
const data = await eventsService.updateKategorie(id, parsed.data);
if (!data) {
res.status(404).json({ success: false, message: 'Kategorie nicht gefunden' });
return;
}
res.json({ success: true, data });
} catch (error) {
logger.error('updateKategorie error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren der Kategorie' });
}
};
// -------------------------------------------------------------------------
// DELETE /api/events/kategorien/:id
// -------------------------------------------------------------------------
deleteKategorie = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params as Record<string, string>;
await eventsService.deleteKategorie(id);
res.json({ success: true, message: 'Kategorie wurde gelöscht' });
} catch (error: any) {
if (
error.message === 'Kategorie nicht gefunden' ||
error.message?.includes('noch Veranstaltungen')
) {
res.status(409).json({ success: false, message: error.message });
return;
}
logger.error('deleteKategorie error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Löschen der Kategorie' });
}
};
// -------------------------------------------------------------------------
// GET /api/events/groups
// -------------------------------------------------------------------------
getAvailableGroups = async (_req: Request, res: Response): Promise<void> => {
res.json({ success: true, data: KNOWN_GROUPS });
};
// -------------------------------------------------------------------------
// GET /api/events/calendar?from=<ISO>&to=<ISO>
// -------------------------------------------------------------------------
getCalendarRange = async (req: Request, res: Response): Promise<void> => {
try {
const fromStr = req.query.from as string | undefined;
const toStr = req.query.to as string | undefined;
if (!fromStr || !toStr) {
res.status(400).json({
success: false,
message: 'Query-Parameter "from" und "to" sind erforderlich (ISO-8601)',
});
return;
}
const from = new Date(fromStr);
const to = new Date(toStr);
if (isNaN(from.getTime()) || isNaN(to.getTime())) {
res.status(400).json({ success: false, message: 'Ungültiges Datumsformat' });
return;
}
if (to < from) {
res.status(400).json({ success: false, message: '"to" muss nach "from" liegen' });
return;
}
const userGroups = getUserGroups(req);
const data = await eventsService.getEventsByDateRange(from, to, userGroups);
res.json({ success: true, data });
} catch (error) {
logger.error('getCalendarRange error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Laden des Kalenders' });
}
};
// -------------------------------------------------------------------------
// GET /api/events/upcoming?limit=10
// -------------------------------------------------------------------------
getUpcoming = async (req: Request, res: Response): Promise<void> => {
try {
const limit = Math.min(Number(req.query.limit ?? 10), 50);
const userGroups = getUserGroups(req);
const data = await eventsService.getUpcomingEvents(limit, userGroups);
res.json({ success: true, data });
} catch (error) {
logger.error('getUpcoming error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Laden der Veranstaltungen' });
}
};
// -------------------------------------------------------------------------
// GET /api/events/:id
// -------------------------------------------------------------------------
getById = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params as Record<string, string>;
const event = await eventsService.getById(id);
if (!event) {
res.status(404).json({ success: false, message: 'Veranstaltung nicht gefunden' });
return;
}
res.json({ success: true, data: event });
} catch (error) {
logger.error('getById error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Laden der Veranstaltung' });
}
};
// -------------------------------------------------------------------------
// POST /api/events
// -------------------------------------------------------------------------
createEvent = async (req: Request, res: Response): Promise<void> => {
try {
const parsed = CreateVeranstaltungSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
success: false,
message: 'Validierungsfehler',
errors: parsed.error.flatten().fieldErrors,
});
return;
}
const data = await eventsService.createEvent(parsed.data, req.user!.id);
res.status(201).json({ success: true, data });
} catch (error) {
logger.error('createEvent error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Erstellen der Veranstaltung' });
}
};
// -------------------------------------------------------------------------
// PATCH /api/events/:id
// -------------------------------------------------------------------------
updateEvent = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params as Record<string, string>;
const parsed = UpdateVeranstaltungSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
success: false,
message: 'Validierungsfehler',
errors: parsed.error.flatten().fieldErrors,
});
return;
}
const data = await eventsService.updateEvent(id, parsed.data);
if (!data) {
res.status(404).json({ success: false, message: 'Veranstaltung nicht gefunden' });
return;
}
res.json({ success: true, data });
} catch (error) {
logger.error('updateEvent error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Aktualisieren der Veranstaltung' });
}
};
// -------------------------------------------------------------------------
// DELETE /api/events/:id (soft cancel)
// -------------------------------------------------------------------------
cancelEvent = async (req: Request, res: Response): Promise<void> => {
try {
const { id } = req.params as Record<string, string>;
const parsed = CancelVeranstaltungSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({
success: false,
message: 'Validierungsfehler',
errors: parsed.error.flatten().fieldErrors,
});
return;
}
await eventsService.cancelEvent(id, parsed.data.abgesagt_grund, req.user!.id);
res.json({ success: true, message: 'Veranstaltung wurde abgesagt' });
} catch (error: any) {
if (error.message === 'Event not found') {
res.status(404).json({ success: false, message: 'Veranstaltung nicht gefunden' });
return;
}
logger.error('cancelEvent error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Absagen der Veranstaltung' });
}
};
// -------------------------------------------------------------------------
// GET /api/events/calendar-token
// -------------------------------------------------------------------------
getCalendarToken = async (req: Request, res: Response): Promise<void> => {
try {
if (!req.user) {
res.status(401).json({ success: false, message: 'Nicht authentifiziert' });
return;
}
const data = await eventsService.getOrCreateIcalToken(req.user.id);
res.json({ success: true, data });
} catch (error) {
logger.error('getCalendarToken error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Laden des Kalender-Tokens' });
}
};
// -------------------------------------------------------------------------
// GET /api/events/calendar.ics?token=<token>
// -------------------------------------------------------------------------
getIcalExport = async (req: Request, res: Response): Promise<void> => {
try {
const token = req.query.token as string | undefined;
if (!token) {
res.status(400).send('Token required');
return;
}
const ical = await eventsService.getIcalExport(token);
if (!ical) {
res.status(404).send('Invalid token');
return;
}
res.setHeader('Content-Type', 'text/calendar; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename="veranstaltungen.ics"');
// 30-minute cache — calendar clients typically re-fetch at this interval
res.setHeader('Cache-Control', 'max-age=1800, public');
res.send(ical);
} catch (error) {
logger.error('getIcalExport error', { error });
res.status(500).json({ success: false, message: 'Fehler beim Erstellen des Kalender-Exports' });
}
};
}
export default new EventsController();