98 lines
2.9 KiB
TypeScript
98 lines
2.9 KiB
TypeScript
/**
|
|
* 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<NextResponse> {
|
|
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<NextResponse> {
|
|
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);
|
|
}
|
|
} |