Files

439 lines
10 KiB
TypeScript

import "server-only";
import { AuditTargetType, InvitationStatus, UserRole } from "@prisma/client";
import { prisma } from "@/lib/prisma";
const roleOrder: UserRole[] = [
UserRole.ADMIN,
UserRole.KOORDINATOR,
UserRole.ERZIEHER,
UserRole.ELTERN,
];
export type KitaOverviewRow = {
id: string;
name: string;
slug: string;
createdAt: Date;
updatedAt: Date;
modules: {
notdienst: boolean;
termine: boolean;
adressbuch: boolean;
};
counts: {
users: number;
families: number;
activeChildren: number;
termine: number;
absences: number;
announcements: number;
openInvitations: number;
lockedUsers: number;
missingConsent: number;
familiesWithoutUsers: number;
};
lastLoginAt: Date | null;
roleCounts: Record<UserRole, number>;
};
export type GlobalAdminHealth = {
orphanTenantUsers: number;
lockedTenantUsers: number;
openInvitations: number;
missingConsent: number;
kitas: number;
};
export async function getGlobalAdminHealth(): Promise<GlobalAdminHealth> {
const now = new Date();
const [
orphanTenantUsers,
lockedTenantUsers,
openInvitations,
missingConsent,
kitas,
] = await Promise.all([
prisma.user.count({
where: {
kitaId: null,
role: { not: UserRole.SUPERADMIN },
},
}),
prisma.user.count({
where: {
role: { not: UserRole.SUPERADMIN },
lockedUntil: { gt: now },
},
}),
prisma.invitation.count({
where: {
status: InvitationStatus.PENDING,
expiresAt: { gt: now },
},
}),
prisma.user.count({
where: {
role: { not: UserRole.SUPERADMIN },
privacyPolicyAcceptedAt: null,
},
}),
prisma.kita.count(),
]);
return {
orphanTenantUsers,
lockedTenantUsers,
openInvitations,
missingConsent,
kitas,
};
}
export async function getKitaOverviewRows(): Promise<KitaOverviewRow[]> {
const now = new Date();
const kitas = await prisma.kita.findMany({
select: {
id: true,
name: true,
slug: true,
notdienstModuleEnabled: true,
terminModuleEnabled: true,
adressbuchModuleEnabled: true,
createdAt: true,
updatedAt: true,
users: {
select: {
role: true,
lastLoginAt: true,
lockedUntil: true,
privacyPolicyAcceptedAt: true,
},
},
families: {
select: {
users: {
select: { id: true },
},
},
},
children: {
select: { active: true },
},
invitations: {
select: {
status: true,
expiresAt: true,
},
},
_count: {
select: {
termine: true,
absences: true,
announcements: true,
},
},
},
orderBy: { name: "asc" },
});
return kitas.map((kita) => {
const roleCounts = Object.fromEntries(
Object.values(UserRole).map((role) => [role, 0]),
) as Record<UserRole, number>;
let lastLoginAt: Date | null = null;
let lockedUsers = 0;
let missingConsent = 0;
for (const user of kita.users) {
roleCounts[user.role] += 1;
if (user.lastLoginAt && (!lastLoginAt || user.lastLoginAt > lastLoginAt)) {
lastLoginAt = user.lastLoginAt;
}
if (user.lockedUntil && user.lockedUntil > now) {
lockedUsers += 1;
}
if (user.role !== UserRole.SUPERADMIN && !user.privacyPolicyAcceptedAt) {
missingConsent += 1;
}
}
return {
id: kita.id,
name: kita.name,
slug: kita.slug,
createdAt: kita.createdAt,
updatedAt: kita.updatedAt,
modules: {
notdienst: kita.notdienstModuleEnabled,
termine: kita.terminModuleEnabled,
adressbuch: kita.adressbuchModuleEnabled,
},
counts: {
users: kita.users.length,
families: kita.families.length,
activeChildren: kita.children.filter((child) => child.active).length,
termine: kita._count.termine,
absences: kita._count.absences,
announcements: kita._count.announcements,
openInvitations: kita.invitations.filter(
(invitation) =>
invitation.status === InvitationStatus.PENDING &&
invitation.expiresAt > now,
).length,
lockedUsers,
missingConsent,
familiesWithoutUsers: kita.families.filter(
(family) => family.users.length === 0,
).length,
},
lastLoginAt,
roleCounts,
};
});
}
export type KitaDiagnostics = {
kita: {
id: string;
name: string;
slug: string;
createdAt: Date;
updatedAt: Date;
modules: KitaOverviewRow["modules"];
};
counts: KitaOverviewRow["counts"] & {
inactiveChildren: number;
expiredPendingInvitations: number;
};
roleCounts: Record<UserRole, number>;
users: {
id: string;
email: string;
firstName: string;
lastName: string;
role: UserRole;
familyName: string | null;
emailVerifiedAt: Date | null;
privacyPolicyAcceptedAt: Date | null;
lastLoginAt: Date | null;
failedLoginAttempts: number;
lockedUntil: Date | null;
createdAt: Date;
}[];
diagnostics: {
lockedUsers: number;
missingConsent: number;
unverifiedUsers: number;
familiesWithoutUsers: number;
usersWithEmptyPassword: number;
};
};
export async function getKitaDiagnostics(
kitaId: string,
): Promise<KitaDiagnostics | null> {
const now = new Date();
const kita = await prisma.kita.findUnique({
where: { id: kitaId },
select: {
id: true,
name: true,
slug: true,
notdienstModuleEnabled: true,
terminModuleEnabled: true,
adressbuchModuleEnabled: true,
createdAt: true,
updatedAt: true,
users: {
select: {
id: true,
email: true,
firstName: true,
lastName: true,
role: true,
passwordHash: true,
emailVerifiedAt: true,
privacyPolicyAcceptedAt: true,
lastLoginAt: true,
failedLoginAttempts: true,
lockedUntil: true,
createdAt: true,
family: {
select: { name: true },
},
},
orderBy: [
{ role: "asc" },
{ lastName: "asc" },
{ firstName: "asc" },
],
},
families: {
select: {
users: { select: { id: true } },
},
},
children: {
select: { active: true },
},
invitations: {
select: {
status: true,
expiresAt: true,
},
},
_count: {
select: {
termine: true,
absences: true,
announcements: true,
},
},
},
});
if (!kita) {
return null;
}
const roleCounts = Object.fromEntries(
Object.values(UserRole).map((role) => [role, 0]),
) as Record<UserRole, number>;
let lockedUsers = 0;
let missingConsent = 0;
let unverifiedUsers = 0;
let usersWithEmptyPassword = 0;
for (const user of kita.users) {
roleCounts[user.role] += 1;
if (user.lockedUntil && user.lockedUntil > now) {
lockedUsers += 1;
}
if (user.role !== UserRole.SUPERADMIN && !user.privacyPolicyAcceptedAt) {
missingConsent += 1;
}
if (!user.emailVerifiedAt) {
unverifiedUsers += 1;
}
if (!user.passwordHash) {
usersWithEmptyPassword += 1;
}
}
const activeChildren = kita.children.filter((child) => child.active).length;
const inactiveChildren = kita.children.length - activeChildren;
const familiesWithoutUsers = kita.families.filter(
(family) => family.users.length === 0,
).length;
return {
kita: {
id: kita.id,
name: kita.name,
slug: kita.slug,
createdAt: kita.createdAt,
updatedAt: kita.updatedAt,
modules: {
notdienst: kita.notdienstModuleEnabled,
termine: kita.terminModuleEnabled,
adressbuch: kita.adressbuchModuleEnabled,
},
},
counts: {
users: kita.users.length,
families: kita.families.length,
activeChildren,
inactiveChildren,
termine: kita._count.termine,
absences: kita._count.absences,
announcements: kita._count.announcements,
openInvitations: kita.invitations.filter(
(invitation) =>
invitation.status === InvitationStatus.PENDING &&
invitation.expiresAt > now,
).length,
expiredPendingInvitations: kita.invitations.filter(
(invitation) =>
invitation.status === InvitationStatus.PENDING &&
invitation.expiresAt <= now,
).length,
lockedUsers,
missingConsent,
familiesWithoutUsers,
},
roleCounts,
users: kita.users.map((user) => ({
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
role: user.role,
familyName: user.family?.name ?? null,
emailVerifiedAt: user.emailVerifiedAt,
privacyPolicyAcceptedAt: user.privacyPolicyAcceptedAt,
lastLoginAt: user.lastLoginAt,
failedLoginAttempts: user.failedLoginAttempts,
lockedUntil: user.lockedUntil,
createdAt: user.createdAt,
})),
diagnostics: {
lockedUsers,
missingConsent,
unverifiedUsers,
familiesWithoutUsers,
usersWithEmptyPassword,
},
};
}
export async function recordKitaDiagnosticView({
actorUserId,
actorEmail,
kitaId,
kitaName,
kitaSlug,
}: {
actorUserId: string;
actorEmail: string;
kitaId: string;
kitaName: string;
kitaSlug: string;
}) {
await prisma.auditLog.create({
data: {
actorUserId,
actorEmail,
action: "KITA_DIAGNOSTIC_VIEW",
targetType: AuditTargetType.KITA,
targetId: kitaId,
targetLabel: `${kitaName} (${kitaSlug})`,
targetKitaId: kitaId,
},
});
}
export async function getAuditLogRows() {
return prisma.auditLog.findMany({
select: {
id: true,
actorEmail: true,
action: true,
targetType: true,
targetId: true,
targetLabel: true,
targetKitaId: true,
metadata: true,
createdAt: true,
},
orderBy: { createdAt: "desc" },
take: 200,
});
}
export { roleOrder };