Rollen-Fix
This commit is contained in:
@@ -21,6 +21,33 @@ const updateUserSchema = z.object({
|
|||||||
approvalPermissions: z.array(approvalPermissionSchema).default([])
|
approvalPermissions: z.array(approvalPermissionSchema).default([])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function serializeManagedUser(user: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
role: "ADMIN" | "FINANCE" | "MEMBER";
|
||||||
|
workingGroupId: string | null;
|
||||||
|
workingGroup: { name: string } | null;
|
||||||
|
approvalPreference: "CHAIR_A" | "CHAIR_B" | "FINANCE" | null;
|
||||||
|
approvalPermissions: ("CHAIR_A" | "CHAIR_B" | "FINANCE")[];
|
||||||
|
_count: {
|
||||||
|
createdExpenses: number;
|
||||||
|
approvals: number;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.username,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role,
|
||||||
|
workingGroupId: user.workingGroupId,
|
||||||
|
workingGroupName: user.workingGroup?.name ?? null,
|
||||||
|
approvalPermissions: normalizeApprovalPermissions(user.role, user.approvalPermissions, user.approvalPreference),
|
||||||
|
createdExpensesCount: user._count.createdExpenses,
|
||||||
|
approvalsCount: user._count.approvals
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
type Context = {
|
type Context = {
|
||||||
params: {
|
params: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -91,7 +118,26 @@ export async function PATCH(request: Request, { params }: Context) {
|
|||||||
role: parsed.data.role,
|
role: parsed.data.role,
|
||||||
workingGroupId,
|
workingGroupId,
|
||||||
approvalPreference,
|
approvalPreference,
|
||||||
approvalPermissions
|
approvalPermissions: {
|
||||||
|
set: approvalPermissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshedUser = await prisma.user.findUniqueOrThrow({
|
||||||
|
where: { id: updatedUser.id },
|
||||||
|
include: {
|
||||||
|
workingGroup: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
approvals: true,
|
||||||
|
createdExpenses: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -113,7 +159,7 @@ export async function PATCH(request: Request, { params }: Context) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({ ok: true });
|
return NextResponse.json({ user: serializeManagedUser(refreshedUser) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(_: Request, { params }: Context) {
|
export async function DELETE(_: Request, { params }: Context) {
|
||||||
|
|||||||
@@ -24,6 +24,33 @@ const createUserSchema = z.object({
|
|||||||
approvalPermissions: z.array(approvalPermissionSchema).default([])
|
approvalPermissions: z.array(approvalPermissionSchema).default([])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function serializeManagedUser(user: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
role: "ADMIN" | "FINANCE" | "MEMBER";
|
||||||
|
workingGroupId: string | null;
|
||||||
|
workingGroup: { name: string } | null;
|
||||||
|
approvalPreference: "CHAIR_A" | "CHAIR_B" | "FINANCE" | null;
|
||||||
|
approvalPermissions: ("CHAIR_A" | "CHAIR_B" | "FINANCE")[];
|
||||||
|
_count: {
|
||||||
|
createdExpenses: number;
|
||||||
|
approvals: number;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.username,
|
||||||
|
username: user.username,
|
||||||
|
role: user.role,
|
||||||
|
workingGroupId: user.workingGroupId,
|
||||||
|
workingGroupName: user.workingGroup?.name ?? null,
|
||||||
|
approvalPermissions: normalizeApprovalPermissions(user.role, user.approvalPermissions, user.approvalPreference),
|
||||||
|
createdExpensesCount: user._count.createdExpenses,
|
||||||
|
approvalsCount: user._count.approvals
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const viewer = await getCurrentViewer();
|
const viewer = await getCurrentViewer();
|
||||||
|
|
||||||
@@ -90,6 +117,23 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createdUser = await prisma.user.findUniqueOrThrow({
|
||||||
|
where: { id: user.id },
|
||||||
|
include: {
|
||||||
|
workingGroup: {
|
||||||
|
select: {
|
||||||
|
name: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_count: {
|
||||||
|
select: {
|
||||||
|
approvals: true,
|
||||||
|
createdExpenses: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await createAuditLog(prisma, {
|
await createAuditLog(prisma, {
|
||||||
actorId: viewer.id,
|
actorId: viewer.id,
|
||||||
action: "user.create",
|
action: "user.create",
|
||||||
@@ -109,11 +153,6 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
user: {
|
user: serializeManagedUser(createdUser)
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
username: user.username,
|
|
||||||
role: user.role
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,23 @@ function toggleApprovalPermission(
|
|||||||
? currentValue.filter((entry) => entry !== approvalType)
|
? currentValue.filter((entry) => entry !== approvalType)
|
||||||
: sortApprovalPermissions([...currentValue, approvalType]);
|
: sortApprovalPermissions([...currentValue, approvalType]);
|
||||||
}
|
}
|
||||||
|
function sortManagedUsersList(users: DashboardManagedUser[]) {
|
||||||
|
const roleOrder: Record<DashboardManagedUser["role"], number> = {
|
||||||
|
ADMIN: 0,
|
||||||
|
FINANCE: 1,
|
||||||
|
MEMBER: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
return [...users].sort((left, right) => {
|
||||||
|
const roleDifference = roleOrder[left.role] - roleOrder[right.role];
|
||||||
|
if (roleDifference !== 0) {
|
||||||
|
return roleDifference;
|
||||||
|
}
|
||||||
|
|
||||||
|
return left.username.localeCompare(right.username, "de-DE");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type MobileSection = "overview" | "actions";
|
type MobileSection = "overview" | "actions";
|
||||||
type DesktopSection = "overview" | "budgetGroups" | "periods" | "users" | "logs";
|
type DesktopSection = "overview" | "budgetGroups" | "periods" | "users" | "logs";
|
||||||
@@ -276,6 +293,7 @@ export function DashboardShell({
|
|||||||
const [editingPasswordUserId, setEditingPasswordUserId] = useState<string | null>(null);
|
const [editingPasswordUserId, setEditingPasswordUserId] = useState<string | null>(null);
|
||||||
const [editingUserId, setEditingUserId] = useState<string | null>(null);
|
const [editingUserId, setEditingUserId] = useState<string | null>(null);
|
||||||
const [passwordDrafts, setPasswordDrafts] = useState<Record<string, string>>({});
|
const [passwordDrafts, setPasswordDrafts] = useState<Record<string, string>>({});
|
||||||
|
const [managedUsersState, setManagedUsersState] = useState(() => sortManagedUsersList(managedUsers));
|
||||||
const [userDrafts, setUserDrafts] = useState<Record<string, ManagedUserDraft>>({});
|
const [userDrafts, setUserDrafts] = useState<Record<string, ManagedUserDraft>>({});
|
||||||
const [approvalThresholdDraft, setApprovalThresholdDraft] = useState(approvalThreshold.toFixed(2));
|
const [approvalThresholdDraft, setApprovalThresholdDraft] = useState(approvalThreshold.toFixed(2));
|
||||||
const [periodForm, setPeriodForm] = useState<PeriodFormState>(getSuggestedPeriodDraft(currentPeriod));
|
const [periodForm, setPeriodForm] = useState<PeriodFormState>(getSuggestedPeriodDraft(currentPeriod));
|
||||||
@@ -422,10 +440,14 @@ export function DashboardShell({
|
|||||||
}, [approvalThreshold]);
|
}, [approvalThreshold]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingUserId && !managedUsers.some((user) => user.id === editingUserId)) {
|
setManagedUsersState(sortManagedUsersList(managedUsers));
|
||||||
|
}, [managedUsers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editingUserId && !managedUsersState.some((user) => user.id === editingUserId)) {
|
||||||
setEditingUserId(null);
|
setEditingUserId(null);
|
||||||
}
|
}
|
||||||
}, [editingUserId, managedUsers]);
|
}, [editingUserId, managedUsersState]);
|
||||||
const selectedExpenseGroup =
|
const selectedExpenseGroup =
|
||||||
editableExpenseGroups.find((group) => group.id === expenseForm.agId) ?? defaultEditableGroup;
|
editableExpenseGroups.find((group) => group.id === expenseForm.agId) ?? defaultEditableGroup;
|
||||||
const selectedBudgetOptions = selectedExpenseGroup?.budgets ?? [];
|
const selectedBudgetOptions = selectedExpenseGroup?.budgets ?? [];
|
||||||
@@ -859,7 +881,7 @@ export function DashboardShell({
|
|||||||
const createdPassword = userForm.password;
|
const createdPassword = userForm.password;
|
||||||
const createdUsername = userForm.username.trim().toLowerCase();
|
const createdUsername = userForm.username.trim().toLowerCase();
|
||||||
|
|
||||||
await parseResponse(
|
const result = (await parseResponse(
|
||||||
await fetch("/api/users", {
|
await fetch("/api/users", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -873,7 +895,13 @@ export function DashboardShell({
|
|||||||
approvalPermissions: sortApprovalPermissions(userForm.approvalPermissions)
|
approvalPermissions: sortApprovalPermissions(userForm.approvalPermissions)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
)) as { user?: DashboardManagedUser };
|
||||||
|
|
||||||
|
if (result.user) {
|
||||||
|
setManagedUsersState((current) =>
|
||||||
|
sortManagedUsersList([...current.filter((user) => user.id !== result.user!.id), result.user!])
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setUserForm({
|
setUserForm({
|
||||||
username: "",
|
username: "",
|
||||||
@@ -897,7 +925,7 @@ export function DashboardShell({
|
|||||||
const draft = getManagedUserDraft(user);
|
const draft = getManagedUserDraft(user);
|
||||||
|
|
||||||
await runAction(async () => {
|
await runAction(async () => {
|
||||||
await parseResponse(
|
const result = (await parseResponse(
|
||||||
await fetch(`/api/users/${user.id}`, {
|
await fetch(`/api/users/${user.id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -909,7 +937,13 @@ export function DashboardShell({
|
|||||||
approvalPermissions: sortApprovalPermissions(draft.approvalPermissions)
|
approvalPermissions: sortApprovalPermissions(draft.approvalPermissions)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
)) as { user?: DashboardManagedUser };
|
||||||
|
|
||||||
|
if (result.user) {
|
||||||
|
setManagedUsersState((current) =>
|
||||||
|
sortManagedUsersList([...current.filter((entry) => entry.id !== result.user!.id), result.user!])
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setEditingUserId(null);
|
setEditingUserId(null);
|
||||||
}, `Nutzer ${user.username} wurde aktualisiert.`);
|
}, `Nutzer ${user.username} wurde aktualisiert.`);
|
||||||
@@ -947,6 +981,18 @@ export function DashboardShell({
|
|||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setManagedUsersState((current) => current.filter((user) => user.id !== userId));
|
||||||
|
setUserDrafts((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[userId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setPasswordDrafts((current) => {
|
||||||
|
const next = { ...current };
|
||||||
|
delete next[userId];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}, "Nutzer wurde gel\u00f6scht.");
|
}, "Nutzer wurde gel\u00f6scht.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1786,7 +1832,7 @@ export function DashboardShell({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Stack spacing={1.4}>
|
<Stack spacing={1.4}>
|
||||||
{managedUsers.map((user) => {
|
{managedUsersState.map((user) => {
|
||||||
const canDelete = user.id !== viewer.id && user.createdExpensesCount === 0 && user.approvalsCount === 0;
|
const canDelete = user.id !== viewer.id && user.createdExpensesCount === 0 && user.approvalsCount === 0;
|
||||||
const isResetOpen = editingPasswordUserId === user.id;
|
const isResetOpen = editingPasswordUserId === user.id;
|
||||||
const isEditingUser = editingUserId === user.id;
|
const isEditingUser = editingUserId === user.id;
|
||||||
|
|||||||
Reference in New Issue
Block a user