/** * PATCH /api/users/[id] - Rolle eines Benutzers aendern (nur ADMIN). * * Sicherheitsregeln: * - Nur ADMIN darf Rollen veraendern. * - Ein Admin kann sich nicht selbst degradieren (verhindert, dass * das letzte Admin-Konto versehentlich die Rechte verliert). * - Der letzte verbleibende Admin kann nicht degradiert werden. */ import { NextRequest, NextResponse } from 'next/server'; import { prisma } from '@/lib/db/client'; import { requireDbUser } from '@/lib/api/context'; import { updateRoleSchema } from '@/lib/validation'; import { errorResponse, forbiddenResponse, notFoundResponse, validationErrorResponse, } from '@/lib/api/responses'; import { withSerializableTx } from '@/lib/db/tx'; import { ZodError } from 'zod'; export async function PATCH( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ): Promise { try { const { id } = await params; const actingUser = await requireDbUser(); if (actingUser.role !== 'ADMIN') { return forbiddenResponse(); } const body = updateRoleSchema.parse(await request.json()); // Selbst-Degradierung blockieren: sonst sperrt sich der Admin // moeglicherweise selbst aus der Verwaltung aus. if (id === actingUser.id && body.role !== 'ADMIN') { return NextResponse.json( { error: 'Die eigene Rolle kann nicht geaendert werden.' }, { status: 409 }, ); } // Letzten Admin schuetzen: Pruefung und Update laufen ATOMAR in // einer SERIALIZABLE-Transaktion. Zwei parallele Degradierungen // koennen sonst beide die Count-Pruefung passieren (TOCTOU) und // die App ohne Admin zuruecklassen. const updated = await withSerializableTx(async (tx) => { const target = await tx.user.findUnique({ where: { id }, select: { id: true, role: true }, }); if (!target) { throw new Error('NOT_FOUND'); } if (target.role === 'ADMIN' && body.role === 'USER') { const adminCount = await tx.user.count({ where: { role: 'ADMIN' } }); if (adminCount <= 1) { throw new Error('LAST_ADMIN'); } } return tx.user.update({ where: { id }, data: { role: body.role }, select: { id: true, email: true, username: true, role: true }, }); }).catch((error: unknown) => { if (error instanceof Error && error.message === 'NOT_FOUND') { return null; } if (error instanceof Error && error.message === 'LAST_ADMIN') { return 'LAST_ADMIN' as const; } throw error; }); if (updated === null) { return notFoundResponse('Benutzer nicht gefunden.'); } if (updated === 'LAST_ADMIN') { return NextResponse.json( { error: 'Der letzte Administrator kann nicht degradiert werden.' }, { status: 409 }, ); } return NextResponse.json({ user: updated }); } catch (error) { if (error instanceof ZodError) { return validationErrorResponse(error); } return errorResponse(error); } }