/** * POST /api/calendars/[id]/invites - Invite-Link erstellen (nur Owner-Admin). * GET /api/calendars/[id]/invites - Invite-Uebersicht (Aktiv/Abgelaufen/Verwendet). */ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db/client'; import { requireCalendarContext } from '@/lib/api/context'; import { createInviteSchema } from '@/lib/validation'; import { canManageCalendar } from '@/lib/permissions/permissions'; import { generateInviteToken, hashInviteToken, defaultInviteExpiry, } from '@/lib/invites/tokens'; import { getConfig } from '@/lib/config'; import { errorResponse, forbiddenResponse } from '@/lib/api/responses'; interface RouteParams { params: Promise<{ id: string }>; } export async function POST( request: NextRequest, { params }: RouteParams, ): Promise { try { const { id } = await params; const context = await requireCalendarContext(id); if (!canManageCalendar(context.user, context.calendar)) { return forbiddenResponse(); } const body = createInviteSchema.parse(await request.json()); const expiry = body.expiresInDays ? new Date(Date.now() + body.expiresInDays * 24 * 60 * 60 * 1000) : defaultInviteExpiry(); // Klartext-Token wird genau einmal zurueckgegeben; in der DB // liegt ausschliesslich der Hash (plan.md Abschnitt 9). const token = generateInviteToken(); const invite = await prisma.invite.create({ data: { calendarId: id, tokenHash: hashInviteToken(token), expiresAt: expiry, }, select: { id: true, expiresAt: true, createdAt: true }, }); return NextResponse.json( { invite, inviteUrl: `${getConfig().appUrl}/invite/${token}`, }, { status: 201 }, ); } catch (error) { return errorResponse(error); } } export async function GET( _request: NextRequest, { params }: RouteParams, ): Promise { try { const { id } = await params; const context = await requireCalendarContext(id); if (!canManageCalendar(context.user, context.calendar)) { return forbiddenResponse(); } const invites = await prisma.invite.findMany({ where: { calendarId: id }, orderBy: { createdAt: 'desc' }, select: { id: true, expiresAt: true, usedAt: true, createdAt: true }, }); const now = new Date(); const summary = { active: invites.filter( (invite) => invite.usedAt === null && (invite.expiresAt === null || invite.expiresAt > now), ).length, expired: invites.filter( (invite) => invite.usedAt === null && invite.expiresAt !== null && invite.expiresAt <= now, ).length, used: invites.filter((invite) => invite.usedAt !== null).length, }; return NextResponse.json({ invites, summary }); } catch (error) { return errorResponse(error); } }