Files
gutscheintracker/app/api/users/invites/route.ts
T

87 lines
4.3 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { getDb } from "@/lib/db";
import { getViewer } from "@/lib/viewer";
import { createInviteToken, hashInviteToken, normalizeEmail, INVITE_VALID_DAYS } from "@/lib/invites";
import { sendMail } from "@/lib/mail";
function inviteUrl(req: NextRequest, rawToken: string): string {
const appUrl = (process.env.APP_URL ?? "").replace(/\/$/, "") || req.nextUrl.origin;
return `${appUrl}/accept-invite?token=${rawToken}`;
}
/** Offene Einladungen: Server-Admin sieht alle, Haushalts-Admin die des eigenen Haushalts. */
export async function GET() {
const viewer = await getViewer();
if (!viewer) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
const db = getDb();
if (viewer.role !== "admin" && !(viewer.householdId !== null && viewer.householdRole === "admin")) {
return NextResponse.json({ error: "forbidden" }, { status: 403 });
}
const where = viewer.role === "admin" ? { acceptedAt: null } : { acceptedAt: null, householdId: viewer.householdId };
const invites = await db.invite.findMany({ where, orderBy: { createdAt: "desc" } });
return NextResponse.json(invites.map(({ id, email, role, householdId, expiresAt, createdAt }) => ({ id, email, role, householdId, expiresAt, createdAt })));
}
/**
* Neue Einladung per E-Mail. Server-Admin wählt den Haushalt (oder lässt ihn
* leer für unzugeordnete Nutzer); Haushalts-Admin lädt immer in den eigenen ein.
*/
export async function POST(req: NextRequest) {
const viewer = await getViewer();
if (!viewer) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
const isServerAdmin = viewer.role === "admin";
const isHouseholdAdmin = viewer.householdId !== null && viewer.householdRole === "admin";
if (!isServerAdmin && !isHouseholdAdmin) return NextResponse.json({ error: "forbidden" }, { status: 403 });
const body = await req.json().catch(() => ({}));
const email = normalizeEmail(typeof body.email === "string" ? body.email : "");
if (!email) return NextResponse.json({ errors: ["email"] }, { status: 400 });
const db = getDb();
if (await db.user.findUnique({ where: { email } })) {
return NextResponse.json({ errors: ["email"], message: "Für diese E-Mail existiert bereits ein Konto." }, { status: 409 });
}
let householdId: number | null = null;
if (isServerAdmin) {
if (body.householdId !== undefined && body.householdId !== null) {
const hid = Number(body.householdId);
if (!Number.isInteger(hid) || !(await db.household.findUnique({ where: { id: hid } }))) {
return NextResponse.json({ errors: ["householdId"] }, { status: 400 });
}
householdId = hid;
}
} else {
householdId = viewer.householdId;
}
const rawToken = createInviteToken();
const expiresAt = new Date(Date.now() + INVITE_VALID_DAYS * 24 * 3600 * 1000);
await db.invite.upsert({
where: { email },
create: { email, tokenHash: hashInviteToken(rawToken), role: "member", householdId, expiresAt },
update: { tokenHash: hashInviteToken(rawToken), expiresAt, acceptedAt: null, householdId },
});
const url = inviteUrl(req, rawToken);
let mailError: string | null = null;
try {
await sendMail(email, "Einladung zum Gutschein-Tracker", `Du wurdest eingeladen. Lege hier dein Konto an (Link 7 Tage gültig): ${url}`);
} catch (e) {
mailError = e instanceof Error ? e.message : String(e);
}
return NextResponse.json({ ok: true, url, mailError }, { status: 201 });
}
/** Einladung zurückziehen. Server-Admin überall, Haushalts-Admin nur im eigenen Haushalt. */
export async function DELETE(req: NextRequest) {
const viewer = await getViewer();
if (!viewer) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
const id = Number(req.nextUrl.searchParams.get("id"));
if (!Number.isInteger(id)) return NextResponse.json({ error: "not found" }, { status: 404 });
const db = getDb();
const existing = await db.invite.findUnique({ where: { id } });
if (!existing) return NextResponse.json({ error: "not found" }, { status: 404 });
const allowed =
viewer.role === "admin" ||
(viewer.householdId !== null && viewer.householdRole === "admin" && existing.householdId === viewer.householdId);
if (!allowed) return NextResponse.json({ error: "forbidden" }, { status: 403 });
await db.invite.delete({ where: { id } });
return NextResponse.json({ ok: true });
}