87 lines
4.3 KiB
TypeScript
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 });
|
|
}
|