feat: show planning availability and manage Kita access roles
This commit is contained in:
+91
-54
@@ -7,6 +7,12 @@ import { AuditTargetType, UserRole } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import { requireRole } from "@/lib/auth-utils";
|
||||
import {
|
||||
assertKitaAdminRemains,
|
||||
lockKitaAccounts,
|
||||
} from "@/lib/kita-admin-guard";
|
||||
import { serializableTransaction } from "@/lib/transaction";
|
||||
import { UserFacingError } from "@/lib/action-error";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
const deleteKitaSchema = z.object({
|
||||
@@ -28,7 +34,8 @@ type ActionResult = {
|
||||
|
||||
async function requireSuperadminActor() {
|
||||
const session = await requireRole([UserRole.SUPERADMIN]);
|
||||
if (Date.now() - session.user.authenticatedAt > 15 * 60_000) redirect("/login?reauth=1&callbackUrl=/admin");
|
||||
if (Date.now() - session.user.authenticatedAt > 15 * 60_000)
|
||||
redirect("/login?reauth=1&callbackUrl=/admin");
|
||||
return {
|
||||
id: session.user.id,
|
||||
email: session.user.email ?? session.user.name ?? "Unbekannter Superadmin",
|
||||
@@ -96,7 +103,10 @@ export async function deleteKita(
|
||||
const counts = await getKitaDeleteCounts(kita.id);
|
||||
|
||||
const attachmentIds = await prisma.$transaction(async (tx) => {
|
||||
const attachments = await tx.attachment.findMany({ where: { announcement: { kitaId: kita.id } }, select: { id: true } });
|
||||
const attachments = await tx.attachment.findMany({
|
||||
where: { announcement: { kitaId: kita.id } },
|
||||
select: { id: true },
|
||||
});
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
actorUserId: actor.id,
|
||||
@@ -111,7 +121,7 @@ export async function deleteKita(
|
||||
});
|
||||
|
||||
await tx.kita.delete({ where: { id: kita.id } });
|
||||
return attachments.map(item => item.id);
|
||||
return attachments.map((item) => item.id);
|
||||
});
|
||||
|
||||
await removeAttachmentFiles(attachmentIds);
|
||||
@@ -156,13 +166,19 @@ async function findMutableTenantUser(userId: string, actorId: string) {
|
||||
return { error: "Benutzer wurde nicht gefunden." } as const;
|
||||
}
|
||||
if (user.id === actorId) {
|
||||
return { error: "Das eigene Superadmin-Konto kann hier nicht geändert werden." } as const;
|
||||
return {
|
||||
error: "Das eigene Superadmin-Konto kann hier nicht geändert werden.",
|
||||
} as const;
|
||||
}
|
||||
if (user.role === UserRole.SUPERADMIN) {
|
||||
return { error: "Andere Superadmins werden in V1 nicht verwaltet." } as const;
|
||||
return {
|
||||
error: "Andere Superadmins werden in V1 nicht verwaltet.",
|
||||
} as const;
|
||||
}
|
||||
if (!user.kitaId) {
|
||||
return { error: "Nur tenant-gebundene Benutzer können verwaltet werden." } as const;
|
||||
return {
|
||||
error: "Nur tenant-gebundene Benutzer können verwaltet werden.",
|
||||
} as const;
|
||||
}
|
||||
|
||||
return { user } as const;
|
||||
@@ -188,34 +204,44 @@ export async function deleteUser(rawPayload: unknown): Promise<ActionResult> {
|
||||
};
|
||||
}
|
||||
|
||||
const attachmentIds = await prisma.$transaction(async (tx) => {
|
||||
const attachments = await tx.attachment.findMany({ where: { announcement: { authorId: user.id } }, select: { id: true } });
|
||||
await tx.verificationToken.deleteMany({
|
||||
where: { identifier: user.id },
|
||||
});
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
actorUserId: actor.id,
|
||||
actorEmail: actor.email,
|
||||
action: "USER_DELETE",
|
||||
targetType: AuditTargetType.USER,
|
||||
targetId: user.id,
|
||||
targetLabel: `${user.firstName} ${user.lastName} <${user.email}>`,
|
||||
targetKitaId: user.kitaId,
|
||||
metadata: {
|
||||
role: user.role,
|
||||
kitaName: user.kita?.name ?? null,
|
||||
kitaSlug: user.kita?.slug ?? null,
|
||||
familyId: user.familyId,
|
||||
familyName: user.family?.name ?? null,
|
||||
familyUserCountBeforeDelete: user.family?._count.users ?? null,
|
||||
let attachmentIds: string[];
|
||||
try {
|
||||
attachmentIds = await serializableTransaction(async (tx) => {
|
||||
await lockKitaAccounts(tx, user.kitaId!);
|
||||
await assertKitaAdminRemains(tx, user.kitaId!, [user.id]);
|
||||
const attachments = await tx.attachment.findMany({
|
||||
where: { announcement: { authorId: user.id } },
|
||||
select: { id: true },
|
||||
});
|
||||
await tx.verificationToken.deleteMany({
|
||||
where: { identifier: user.id },
|
||||
});
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
actorUserId: actor.id,
|
||||
actorEmail: actor.email,
|
||||
action: "USER_DELETE",
|
||||
targetType: AuditTargetType.USER,
|
||||
targetId: user.id,
|
||||
targetLabel: `${user.firstName} ${user.lastName} <${user.email}>`,
|
||||
targetKitaId: user.kitaId,
|
||||
metadata: {
|
||||
role: user.role,
|
||||
kitaName: user.kita?.name ?? null,
|
||||
kitaSlug: user.kita?.slug ?? null,
|
||||
familyId: user.familyId,
|
||||
familyName: user.family?.name ?? null,
|
||||
familyUserCountBeforeDelete: user.family?._count.users ?? null,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await tx.user.delete({ where: { id: user.id } });
|
||||
return attachments.map((item) => item.id);
|
||||
});
|
||||
await tx.user.delete({ where: { id: user.id } });
|
||||
return attachments.map(item => item.id);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (!(error instanceof UserFacingError)) throw error;
|
||||
return { ok: false, error: error.message };
|
||||
}
|
||||
await removeAttachmentFiles(attachmentIds);
|
||||
revalidatePath("/admin");
|
||||
revalidatePath(`/admin/kitas/${user.kitaId}`);
|
||||
@@ -245,29 +271,41 @@ async function updateUserSupportState(
|
||||
|
||||
const { user } = lookup;
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.user.update({
|
||||
where: { id: user.id },
|
||||
data: { ...data, ...(action === "USER_LOCK" ? { sessionVersion: { increment: 1 } } : {}) },
|
||||
});
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
actorUserId: actor.id,
|
||||
actorEmail: actor.email,
|
||||
action,
|
||||
targetType: AuditTargetType.USER,
|
||||
targetId: user.id,
|
||||
targetLabel: `${user.firstName} ${user.lastName} <${user.email}>`,
|
||||
targetKitaId: user.kitaId,
|
||||
metadata: {
|
||||
role: user.role,
|
||||
previousLockedUntil: user.lockedUntil?.toISOString() ?? null,
|
||||
previousFailedLoginAttempts: user.failedLoginAttempts,
|
||||
try {
|
||||
await serializableTransaction(async (tx) => {
|
||||
await lockKitaAccounts(tx, user.kitaId!);
|
||||
if (action === "USER_LOCK")
|
||||
await assertKitaAdminRemains(tx, user.kitaId!, [user.id]);
|
||||
await tx.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
...data,
|
||||
...(action === "USER_LOCK"
|
||||
? { sessionVersion: { increment: 1 } }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
actorUserId: actor.id,
|
||||
actorEmail: actor.email,
|
||||
action,
|
||||
targetType: AuditTargetType.USER,
|
||||
targetId: user.id,
|
||||
targetLabel: `${user.firstName} ${user.lastName} <${user.email}>`,
|
||||
targetKitaId: user.kitaId,
|
||||
metadata: {
|
||||
role: user.role,
|
||||
previousLockedUntil: user.lockedUntil?.toISOString() ?? null,
|
||||
previousFailedLoginAttempts: user.failedLoginAttempts,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
if (!(error instanceof UserFacingError)) throw error;
|
||||
return { ok: false, error: error.message };
|
||||
}
|
||||
revalidatePath("/admin");
|
||||
revalidatePath(`/admin/kitas/${user.kitaId}`);
|
||||
revalidatePath("/admin/audit");
|
||||
@@ -296,4 +334,3 @@ export async function resetFailedLoginAttempts(
|
||||
failedLoginAttempts: 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user