In der Nutzerverwaltung kannst du jetzt pro Konto die Rolle, die AG-Zuordnung und die Freigaberollen bearbeiten. Die feste 3er-Freigabelogik bleibt Vorstand A / Vorstand B / Finanz-AG, aber jetzt legst du über die Nutzer fest, wer diese Schritte autorisieren darf. Zusätzlich gibt es unter Nutzer anlegen eine eigene Insel für die Freigabe-Schwelle, und diese Schwelle wird jetzt auch wirklich überall verwendet: in der Erfassungslogik, in den Budgetkarten, im CSV-Backup/-Import und im Audit-Restore. Die Hauptänderungen sitzen in dashboard-shell.tsx, budget-column.tsx, route.ts, schema.prisma und route.ts.
All checks were successful
CI / build-and-deploy (push) Successful in 1m22s
All checks were successful
CI / build-and-deploy (push) Successful in 1m22s
Den Zeitraum-Bereich habe ich dabei gleich mit aufgeräumt: die Auswahl des aktuellen Haushalts ist breiter und sauberer angeordnet, und die Desktop-Nutzerverwaltung ist jetzt wirklich links Anlegen + Schwelle und rechts die Nutzerliste. Seed und Backup/Restore kennen die neuen Felder ebenfalls in seed.ts, route.ts und route.ts.
This commit is contained in:
@@ -1,17 +1,121 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
import { snapshotUser } from "@/lib/audit-snapshots";
|
||||
import { createAuditLog } from "@/lib/audit-log";
|
||||
import { canManageUsers } from "@/lib/domain";
|
||||
import {
|
||||
APPROVAL_FLOW,
|
||||
canManageUsers,
|
||||
getLegacyApprovalPreference,
|
||||
normalizeApprovalPermissions
|
||||
} from "@/lib/domain";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { getCurrentViewer } from "@/lib/session";
|
||||
|
||||
const userRoleSchema = z.enum(["ADMIN", "FINANCE", "MEMBER"]);
|
||||
const approvalPermissionSchema = z.enum(APPROVAL_FLOW);
|
||||
|
||||
const updateUserSchema = z.object({
|
||||
role: userRoleSchema,
|
||||
workingGroupId: z.union([z.string().trim().min(1), z.literal(""), z.null(), z.undefined()]),
|
||||
approvalPermissions: z.array(approvalPermissionSchema).default([])
|
||||
});
|
||||
|
||||
type Context = {
|
||||
params: {
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function PATCH(request: Request, { params }: Context) {
|
||||
const viewer = await getCurrentViewer();
|
||||
|
||||
if (!viewer) {
|
||||
return NextResponse.json({ error: "Nicht angemeldet." }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!canManageUsers(viewer.role)) {
|
||||
return NextResponse.json({ error: "Nur Vorstand oder Finanz-AG duerfen Nutzer bearbeiten." }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = updateUserSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Bitte Rolle, AG und Freigaberollen korrekt angeben." }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: params.id }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Nutzer nicht gefunden." }, { status: 404 });
|
||||
}
|
||||
|
||||
const workingGroupId = typeof parsed.data.workingGroupId === "string" && parsed.data.workingGroupId.length > 0
|
||||
? parsed.data.workingGroupId
|
||||
: null;
|
||||
|
||||
if (parsed.data.role === "MEMBER" && !workingGroupId) {
|
||||
return NextResponse.json({ error: "AG-Mitglieder brauchen eine AG-Zuordnung." }, { status: 400 });
|
||||
}
|
||||
|
||||
if (workingGroupId) {
|
||||
const workingGroup = await prisma.workingGroup.findUnique({
|
||||
where: { id: workingGroupId }
|
||||
});
|
||||
|
||||
if (!workingGroup) {
|
||||
return NextResponse.json({ error: "Die ausgewaehlte AG wurde nicht gefunden." }, { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
if (user.role === "ADMIN" && parsed.data.role !== "ADMIN") {
|
||||
const adminCount = await prisma.user.count({
|
||||
where: { role: "ADMIN" }
|
||||
});
|
||||
|
||||
if (adminCount <= 1) {
|
||||
return NextResponse.json({ error: "Mindestens ein Vorstandskonto muss erhalten bleiben." }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const approvalPermissions = normalizeApprovalPermissions(parsed.data.role, parsed.data.approvalPermissions, null);
|
||||
const approvalPreference = getLegacyApprovalPreference(approvalPermissions);
|
||||
const previousSnapshot = snapshotUser(user);
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: params.id },
|
||||
data: {
|
||||
role: parsed.data.role,
|
||||
workingGroupId,
|
||||
approvalPreference,
|
||||
approvalPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await createAuditLog(prisma, {
|
||||
actorId: viewer.id,
|
||||
action: "user.update",
|
||||
entityType: "user",
|
||||
entityId: updatedUser.id,
|
||||
entityLabel: updatedUser.username,
|
||||
summary: `Nutzer ${updatedUser.username} wurde aktualisiert.`,
|
||||
metadata: {
|
||||
workingGroupId: updatedUser.workingGroupId,
|
||||
role: updatedUser.role,
|
||||
approvalPermissions: updatedUser.approvalPermissions,
|
||||
rollback: {
|
||||
kind: "user.update",
|
||||
previous: previousSnapshot
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
export async function DELETE(_: Request, { params }: Context) {
|
||||
const viewer = await getCurrentViewer();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user