Files
vereinskalender/app/api/users/route.ts
2026-01-18 00:40:01 +01:00

370 lines
9.7 KiB
TypeScript

import { NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import { prisma } from "../../../lib/prisma";
import {
isAdminSession,
isSuperAdminSession,
requireSession
} from "../../../lib/auth-helpers";
export async function GET(request: Request) {
const { session } = await requireSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!isAdminSession(session)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { searchParams } = new URL(request.url);
const status = searchParams.get("status");
const users = await prisma.user.findMany({
where: status ? { status } : undefined,
orderBy: { createdAt: "desc" },
select: {
id: true,
email: true,
name: true,
status: true,
role: true,
emailVerified: true,
createdAt: true
}
});
if (!isSuperAdminSession(session)) {
return NextResponse.json(users);
}
const emails = users.map((user) => user.email).filter(Boolean);
const attempts = emails.length
? await prisma.loginAttempt.findMany({
where: { email: { in: emails } }
})
: [];
const stats = attempts.reduce<Record<string, {
attempts: number;
lastAttempt: Date | null;
lockedUntil: Date | null;
}>>((acc, attempt) => {
const current = acc[attempt.email] || {
attempts: 0,
lastAttempt: null,
lockedUntil: null
};
current.attempts += attempt.attempts;
if (!current.lastAttempt || attempt.lastAttempt > current.lastAttempt) {
current.lastAttempt = attempt.lastAttempt;
}
if (!current.lockedUntil || (attempt.lockedUntil && attempt.lockedUntil > current.lockedUntil)) {
current.lockedUntil = attempt.lockedUntil;
}
acc[attempt.email] = current;
return acc;
}, {});
const enriched = users.map((user) => ({
...user,
loginStats: stats[user.email] || {
attempts: 0,
lastAttempt: null,
lockedUntil: null
}
}));
return NextResponse.json(enriched);
}
export async function POST(request: Request) {
const { session } = await requireSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!isAdminSession(session)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const body = await request.json();
const {
email,
name,
password,
role,
status,
emailVerified
} = body || {};
if (!email || !password) {
return NextResponse.json(
{ error: "E-Mail und Passwort sind erforderlich." },
{ status: 400 }
);
}
const normalizedEmail = String(email).trim().toLowerCase();
const allowedRoles = ["USER", "ADMIN", "SUPERADMIN"];
const allowedStatuses = ["ACTIVE", "PENDING", "DISABLED"];
const isSuperAdmin = isSuperAdminSession(session);
const nextRole = isSuperAdmin && allowedRoles.includes(role) ? role : "USER";
const nextStatus = allowedStatuses.includes(status) ? status : "PENDING";
if (!normalizedEmail) {
return NextResponse.json({ error: "Ungültige E-Mail." }, { status: 400 });
}
const passwordHash = await bcrypt.hash(String(password), 10);
try {
const user = await prisma.user.create({
data: {
email: normalizedEmail,
name: name ? String(name).trim() : null,
passwordHash,
role: nextRole,
status: nextStatus,
emailVerified: Boolean(emailVerified)
},
select: {
id: true,
email: true,
name: true,
role: true,
status: true,
emailVerified: true,
createdAt: true
}
});
return NextResponse.json(user, { status: 201 });
} catch (err) {
return NextResponse.json(
{ error: "Benutzer konnte nicht angelegt werden." },
{ status: 400 }
);
}
}
export async function PATCH(request: Request) {
const { session } = await requireSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!isAdminSession(session)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const body = await request.json();
const {
userId,
status,
role,
name,
email,
password,
emailVerified,
resetLoginAttempts
} = body || {};
if (!userId) {
return NextResponse.json({ error: "Ungültige Anfrage." }, { status: 400 });
}
const target = await prisma.user.findUnique({
where: { id: userId },
select: { role: true }
});
if (!target) {
return NextResponse.json({ error: "Benutzer nicht gefunden." }, { status: 404 });
}
if (resetLoginAttempts) {
if (!isSuperAdminSession(session)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const userRecord = await prisma.user.findUnique({
where: { id: userId },
select: { email: true }
});
if (userRecord?.email) {
await prisma.loginAttempt.deleteMany({ where: { email: userRecord.email } });
}
return NextResponse.json({ ok: true });
}
if (target.role === "SUPERADMIN" && !isSuperAdminSession(session)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const allowedStatuses = ["ACTIVE", "PENDING", "DISABLED"];
const data: Record<string, any> = {};
if (status) {
if (!allowedStatuses.includes(status)) {
return NextResponse.json({ error: "Ungültiger Status." }, { status: 400 });
}
data.status = status;
}
if (name !== undefined) {
data.name = name ? String(name).trim() : null;
}
if (email) {
data.email = String(email).trim().toLowerCase();
}
if (emailVerified !== undefined) {
data.emailVerified = Boolean(emailVerified);
}
if (password) {
data.passwordHash = await bcrypt.hash(String(password), 10);
}
if (role) {
if (!isSuperAdminSession(session)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
if (!["USER", "ADMIN", "SUPERADMIN"].includes(role)) {
return NextResponse.json({ error: "Ungültige Rolle." }, { status: 400 });
}
data.role = role;
}
const user = await prisma.user.update({
where: { id: userId },
data,
select: {
id: true,
email: true,
name: true,
role: true,
status: true,
emailVerified: true,
createdAt: true
}
});
return NextResponse.json({ id: user.id, status: user.status });
}
export async function DELETE(request: Request) {
const { session } = await requireSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!isAdminSession(session)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const { searchParams } = new URL(request.url);
const userId = searchParams.get("id");
const hardDelete = searchParams.get("hard") === "true";
if (!userId) {
return NextResponse.json({ error: "Ungültige Anfrage." }, { status: 400 });
}
if (session.user?.id === userId) {
return NextResponse.json(
{ error: "Eigenes Konto kann nicht gelöscht werden." },
{ status: 400 }
);
}
const target = await prisma.user.findUnique({
where: { id: userId },
select: { role: true }
});
if (!target) {
return NextResponse.json({ error: "Benutzer nicht gefunden." }, { status: 404 });
}
if (target.role !== "USER" && !isSuperAdminSession(session)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
if (hardDelete) {
if (!isSuperAdminSession(session)) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const user = await prisma.user.findUnique({
where: { id: userId },
select: { email: true }
});
if (!user) {
return NextResponse.json({ error: "Benutzer nicht gefunden." }, { status: 404 });
}
const events = await prisma.event.findMany({
where: { createdById: userId },
select: { id: true }
});
const eventIds = events.map((event) => event.id);
const views = await prisma.userView.findMany({
where: { userId },
select: { id: true }
});
const viewIds = views.map((view) => view.id);
await prisma.$transaction(async (tx) => {
if (eventIds.length > 0) {
await tx.userViewItem.deleteMany({
where: { eventId: { in: eventIds } }
});
await tx.userViewExclusion.deleteMany({
where: { eventId: { in: eventIds } }
});
await tx.event.deleteMany({
where: { id: { in: eventIds } }
});
}
if (viewIds.length > 0) {
await tx.userViewItem.deleteMany({
where: { viewId: { in: viewIds } }
});
await tx.userViewCategory.deleteMany({
where: { viewId: { in: viewIds } }
});
await tx.userViewExclusion.deleteMany({
where: { viewId: { in: viewIds } }
});
await tx.userView.deleteMany({
where: { id: { in: viewIds } }
});
}
await tx.session.deleteMany({ where: { userId } });
await tx.account.deleteMany({ where: { userId } });
await tx.passwordResetToken.deleteMany({ where: { userId } });
await tx.loginAttempt.deleteMany({ where: { email: user.email } });
await tx.verificationToken.deleteMany({ where: { identifier: user.email } });
await tx.user.delete({ where: { id: userId } });
});
return NextResponse.json({ ok: true, deleted: true });
}
await prisma.session.deleteMany({ where: { userId } });
await prisma.account.deleteMany({ where: { userId } });
await prisma.user.update({
where: { id: userId },
data: { status: "DISABLED", emailVerified: false }
});
return NextResponse.json({ ok: true });
}