121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
/**
|
|
* GET /api/calendars/[id]/reservations - Reservierungen eines Kalenders.
|
|
* POST /api/calendars/[id]/reservations - neue Reservierung (atomar).
|
|
*
|
|
* Berechtigungen werden serverseitig ueber die zentrale
|
|
* Berechtigungsschicht erzwungen (plan.md Abschnitt 2 und 20).
|
|
*
|
|
* Sichtbarkeiten: Titel und Notizen fremder Reservierungen werden
|
|
* fuer Nicht-Admins gemaess der Kalender-Einstellungen geschwaerzt
|
|
* (show_title_to_members / show_notes_to_members).
|
|
*/
|
|
import { NextRequest, NextResponse } from 'next/server';
|
|
import { prisma } from '@/lib/db/client';
|
|
import { requireCalendarContext } from '@/lib/api/context';
|
|
import { createReservationSchema } from '@/lib/validation';
|
|
import { canCreateReservation } from '@/lib/permissions/permissions';
|
|
import { createReservationAtomically } from '@/lib/reservations/service';
|
|
import { errorResponse, forbiddenResponse } from '@/lib/api/responses';
|
|
import { displayName } from '@/lib/privacy/display';
|
|
|
|
interface RouteParams {
|
|
params: Promise<{ id: string }>;
|
|
}
|
|
|
|
export async function GET(
|
|
request: NextRequest,
|
|
{ params }: RouteParams,
|
|
): Promise<NextResponse> {
|
|
try {
|
|
const { id } = await params;
|
|
const context = await requireCalendarContext(id);
|
|
|
|
// Optionales Zeitfenster-Filter (Monat/Woche/Tag-Ansicht).
|
|
// Overlap-Bedingung (startAt < to UND endAt > from) statt Enthaltensein:
|
|
// Eine Reservierung muss zurueckgegeben werden, wenn sie den
|
|
// Zeitraum auch nur teilweise ueberdeckt (z. B. Mehrtages-Reservierung,
|
|
// die vor dem Fenster beginnt oder danach endet).
|
|
const from = request.nextUrl.searchParams.get('from');
|
|
const to = request.nextUrl.searchParams.get('to');
|
|
|
|
const reservations = await prisma.reservation.findMany({
|
|
where: {
|
|
calendarId: id,
|
|
status: 'CONFIRMED',
|
|
...(from && to
|
|
? { startAt: { lt: new Date(to) }, endAt: { gt: new Date(from) } }
|
|
: {}),
|
|
},
|
|
orderBy: { startAt: 'asc' },
|
|
select: {
|
|
id: true,
|
|
userId: true,
|
|
title: true,
|
|
startAt: true,
|
|
endAt: true,
|
|
status: true,
|
|
notes: true,
|
|
user: { select: { email: true, username: true } },
|
|
},
|
|
});
|
|
|
|
const showForeignTitles =
|
|
context.user.role === 'ADMIN' || context.calendar.showTitleToMembers;
|
|
const showForeignNotes =
|
|
context.user.role === 'ADMIN' || context.calendar.showNotesToMembers;
|
|
|
|
return NextResponse.json({
|
|
reservations: reservations.map((reservation) => {
|
|
const isOwn = reservation.userId === context.user.id;
|
|
return {
|
|
id: reservation.id,
|
|
userId: reservation.userId,
|
|
title: isOwn || showForeignTitles ? reservation.title : 'Gebucht',
|
|
startAt: reservation.startAt,
|
|
endAt: reservation.endAt,
|
|
status: reservation.status,
|
|
notes: isOwn || showForeignNotes ? reservation.notes : null,
|
|
// Datenschutz: Keine vollstaendigen E-Mail-Adressen an
|
|
// Mitglieder; nur Username oder maskierte E-Mail.
|
|
ownerName: displayName(
|
|
reservation.user.username,
|
|
reservation.user.email,
|
|
),
|
|
};
|
|
}),
|
|
});
|
|
} catch (error) {
|
|
return errorResponse(error);
|
|
}
|
|
}
|
|
|
|
export async function POST(
|
|
request: NextRequest,
|
|
{ params }: RouteParams,
|
|
): Promise<NextResponse> {
|
|
try {
|
|
const { id } = await params;
|
|
const context = await requireCalendarContext(id);
|
|
|
|
if (!canCreateReservation(context.user, context.calendar, context.isMember)) {
|
|
return forbiddenResponse();
|
|
}
|
|
|
|
const body = createReservationSchema.parse(await request.json());
|
|
|
|
// Serverseitige, atomare Pruefung (plan.md Abschnitt 6 und 15):
|
|
// Kalender gueltig? Benutzer berechtigt? Max-Dauer? Zeit verfuegbar?
|
|
const reservation = await createReservationAtomically({
|
|
calendarId: id,
|
|
userId: context.user.id,
|
|
title: body.title,
|
|
startAt: body.startAt,
|
|
endAt: body.endAt,
|
|
notes: body.notes ?? null,
|
|
});
|
|
|
|
return NextResponse.json({ reservation }, { status: 201 });
|
|
} catch (error) {
|
|
return errorResponse(error);
|
|
}
|
|
} |