bug fix for atemschutz
This commit is contained in:
270
backend/src/controllers/booking.controller.ts
Normal file
270
backend/src/controllers/booking.controller.ts
Normal file
@@ -0,0 +1,270 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { ZodError } from 'zod';
|
||||
import bookingService from '../services/booking.service';
|
||||
import {
|
||||
CreateBuchungSchema,
|
||||
UpdateBuchungSchema,
|
||||
CancelBuchungSchema,
|
||||
} from '../models/booking.model';
|
||||
import logger from '../utils/logger';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isValidUUID(s: string): boolean {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s);
|
||||
}
|
||||
|
||||
function handleZodError(res: Response, err: ZodError): void {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validierungsfehler',
|
||||
errors: err.flatten().fieldErrors,
|
||||
});
|
||||
}
|
||||
|
||||
function handleConflictError(res: Response, err: Error): boolean {
|
||||
if (err.message?.includes('bereits gebucht')) {
|
||||
res.status(409).json({ success: false, message: err.message });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Controller
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class BookingController {
|
||||
/**
|
||||
* GET /api/bookings/calendar?from=&to=&fahrzeugId=
|
||||
* Returns all non-cancelled bookings overlapping the given date range.
|
||||
*/
|
||||
async getCalendarRange(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { from, to, fahrzeugId } = req.query;
|
||||
if (!from || !to) {
|
||||
res.status(400).json({ success: false, message: 'from und to sind erforderlich' });
|
||||
return;
|
||||
}
|
||||
const bookings = await bookingService.getBookingsByRange(
|
||||
new Date(from as string),
|
||||
new Date(to as string),
|
||||
fahrzeugId as string | undefined
|
||||
);
|
||||
res.json({ success: true, data: bookings });
|
||||
} catch (error) {
|
||||
logger.error('Booking getCalendarRange error', { error });
|
||||
res.status(500).json({ success: false, message: 'Buchungen konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/bookings/upcoming?limit=
|
||||
* Returns the next upcoming non-cancelled bookings.
|
||||
*/
|
||||
async getUpcoming(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit as string) || 20;
|
||||
const bookings = await bookingService.getUpcoming(limit);
|
||||
res.json({ success: true, data: bookings });
|
||||
} catch (error) {
|
||||
logger.error('Booking getUpcoming error', { error });
|
||||
res.status(500).json({ success: false, message: 'Buchungen konnten nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/bookings/availability?fahrzeugId=&from=&to=
|
||||
* Returns { available: true } when the vehicle has no conflicting booking.
|
||||
*/
|
||||
async checkAvailability(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { fahrzeugId, from, to } = req.query;
|
||||
if (!fahrzeugId || !from || !to) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, message: 'fahrzeugId, from und to sind erforderlich' });
|
||||
return;
|
||||
}
|
||||
const hasConflict = await bookingService.checkConflict(
|
||||
fahrzeugId as string,
|
||||
new Date(from as string),
|
||||
new Date(to as string)
|
||||
);
|
||||
res.json({ success: true, data: { available: !hasConflict } });
|
||||
} catch (error) {
|
||||
logger.error('Booking checkAvailability error', { error });
|
||||
res.status(500).json({ success: false, message: 'Verfügbarkeit konnte nicht geprüft werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/bookings/:id
|
||||
* Returns a single booking with all joined fields.
|
||||
*/
|
||||
async getById(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params as Record<string, string>;
|
||||
if (!isValidUUID(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige Buchungs-ID' });
|
||||
return;
|
||||
}
|
||||
const booking = await bookingService.getById(id);
|
||||
if (!booking) {
|
||||
res.status(404).json({ success: false, message: 'Buchung nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: booking });
|
||||
} catch (error) {
|
||||
logger.error('Booking getById error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Buchung konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/bookings
|
||||
* Creates a new vehicle booking.
|
||||
*/
|
||||
async create(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const parsed = CreateBuchungSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
handleZodError(res, parsed.error);
|
||||
return;
|
||||
}
|
||||
const booking = await bookingService.create(parsed.data, req.user!.id);
|
||||
res.status(201).json({ success: true, data: booking });
|
||||
} catch (error: any) {
|
||||
if (handleConflictError(res, error)) return;
|
||||
logger.error('Booking create error', { error });
|
||||
res.status(500).json({ success: false, message: 'Buchung konnte nicht erstellt werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/bookings/:id
|
||||
* Updates the provided fields of an existing booking.
|
||||
*/
|
||||
async update(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params as Record<string, string>;
|
||||
if (!isValidUUID(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige Buchungs-ID' });
|
||||
return;
|
||||
}
|
||||
const parsed = UpdateBuchungSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
handleZodError(res, parsed.error);
|
||||
return;
|
||||
}
|
||||
if (Object.keys(parsed.data).length === 0) {
|
||||
res.status(400).json({ success: false, message: 'Kein Feld zum Aktualisieren angegeben' });
|
||||
return;
|
||||
}
|
||||
const booking = await bookingService.update(id, parsed.data);
|
||||
if (!booking) {
|
||||
res.status(404).json({ success: false, message: 'Buchung nicht gefunden' });
|
||||
return;
|
||||
}
|
||||
res.json({ success: true, data: booking });
|
||||
} catch (error: any) {
|
||||
if (error?.message === 'No fields to update') {
|
||||
res.status(400).json({ success: false, message: 'Kein Feld zum Aktualisieren angegeben' });
|
||||
return;
|
||||
}
|
||||
if (handleConflictError(res, error)) return;
|
||||
logger.error('Booking update error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Buchung konnte nicht aktualisiert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/bookings/:id
|
||||
* Soft-cancels a booking (sets abgesagt=TRUE).
|
||||
*/
|
||||
async cancel(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params as Record<string, string>;
|
||||
if (!isValidUUID(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige Buchungs-ID' });
|
||||
return;
|
||||
}
|
||||
const parsed = CancelBuchungSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
handleZodError(res, parsed.error);
|
||||
return;
|
||||
}
|
||||
await bookingService.cancel(id, parsed.data.abgesagt_grund);
|
||||
res.json({ success: true, message: 'Buchung wurde storniert' });
|
||||
} catch (error) {
|
||||
logger.error('Booking cancel error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Buchung konnte nicht storniert werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/bookings/:id/force
|
||||
* Hard-deletes a booking record (admin only).
|
||||
*/
|
||||
async hardDelete(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { id } = req.params as Record<string, string>;
|
||||
if (!isValidUUID(id)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige Buchungs-ID' });
|
||||
return;
|
||||
}
|
||||
await bookingService.delete(id);
|
||||
res.json({ success: true, message: 'Buchung gelöscht' });
|
||||
} catch (error) {
|
||||
logger.error('Booking hardDelete error', { error, id: req.params.id });
|
||||
res.status(500).json({ success: false, message: 'Buchung konnte nicht gelöscht werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/bookings/calendar-token
|
||||
* Returns the user's iCal subscribe token and URL, creating it if needed.
|
||||
*/
|
||||
async getCalendarToken(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const result = await bookingService.getOrCreateIcalToken(req.user!.id);
|
||||
res.json({ success: true, data: result });
|
||||
} catch (error) {
|
||||
logger.error('Booking getCalendarToken error', { error });
|
||||
res.status(500).json({ success: false, message: 'Kalender-Token konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/bookings/calendar.ics?token=&fahrzeugId=
|
||||
* Returns an iCal file for the subscriber. No authentication required
|
||||
* (token-based access).
|
||||
*/
|
||||
async getIcalExport(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const { token, fahrzeugId } = req.query;
|
||||
if (!token) {
|
||||
res.status(400).send('Token required');
|
||||
return;
|
||||
}
|
||||
const ical = await bookingService.getIcalExport(
|
||||
token as string,
|
||||
fahrzeugId as string | undefined
|
||||
);
|
||||
if (!ical) {
|
||||
res.status(404).send('Invalid token');
|
||||
return;
|
||||
}
|
||||
res.setHeader('Content-Type', 'text/calendar; charset=utf-8');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename="fahrzeugbuchungen.ics"');
|
||||
res.send(ical);
|
||||
} catch (error) {
|
||||
logger.error('Booking getIcalExport error', { error });
|
||||
res.status(500).send('Internal server error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new BookingController();
|
||||
321
backend/src/controllers/events.controller.ts
Normal file
321
backend/src/controllers/events.controller.ts
Normal 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();
|
||||
Reference in New Issue
Block a user