Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/services/tenantUserLifecycle.server.ts
T

97 lines
3.3 KiB
TypeScript

import { TenantUserStatus } from "~/application/enums/tenants/TenantUserStatus";
import { TenantUserType } from "~/application/enums/tenants/TenantUserType";
import { db } from "~/utils/db.server";
// R-P2b — User-Lifecycle: Soft-Deaktivierung mit Last-Admin-Guard + optionalem Ownership-Reassign.
// Ein Tenant muss IMMER mindestens einen aktiven Owner/Admin behalten, sonst waere er handlungsunfaehig.
const PRIVILEGED_TYPES = [TenantUserType.OWNER, TenantUserType.ADMIN];
export type DeactivateTenantUserResult =
| { ok: true }
| { ok: false; reason: "not-found" | "already-inactive" | "last-admin" };
// Zaehlt aktive Owner/Admins eines Tenants (optional einen User ausschliessend).
export async function countActivePrivilegedMembers(tenantId: string, excludeUserId?: string): Promise<number> {
return db.tenantUser.count({
where: {
tenantId,
status: TenantUserStatus.ACTIVE,
type: { in: PRIVILEGED_TYPES },
...(excludeUserId ? { userId: { not: excludeUserId } } : {}),
},
});
}
// Reassign des tenant-eigenen Besitzes eines deaktivierten Users an einen Nachfolger. Bei P2 ist das
// die Gruppen-Ownership; Fach-Module (CRM/Projekte ab P6) haengen ihren Reassign hier an, wenn sie
// existieren. Gibt die Anzahl umgehaengter Datensaetze zurueck.
export async function reassignTenantUserOwnership({
tenantId,
fromUserId,
toUserId,
}: {
tenantId: string;
fromUserId: string;
toUserId: string;
}): Promise<number> {
const groups = await db.group.updateMany({
where: { tenantId, createdByUserId: fromUserId },
data: { createdByUserId: toUserId },
});
return groups.count;
}
export async function deactivateTenantUser({
tenantId,
targetUserId,
reassignToUserId,
}: {
tenantId: string;
targetUserId: string;
reassignToUserId?: string;
}): Promise<DeactivateTenantUserResult> {
const member = await db.tenantUser.findFirst({
where: { tenantId, userId: targetUserId },
select: { id: true, type: true, status: true },
});
if (!member) {
return { ok: false, reason: "not-found" };
}
if (member.status === TenantUserStatus.INACTIVE) {
return { ok: false, reason: "already-inactive" };
}
if (PRIVILEGED_TYPES.includes(member.type)) {
const otherActiveAdmins = await countActivePrivilegedMembers(tenantId, targetUserId);
if (otherActiveAdmins === 0) {
return { ok: false, reason: "last-admin" };
}
}
if (reassignToUserId && reassignToUserId !== targetUserId) {
await reassignTenantUserOwnership({ tenantId, fromUserId: targetUserId, toUserId: reassignToUserId });
}
await db.tenantUser.update({ where: { id: member.id }, data: { status: TenantUserStatus.INACTIVE } });
return { ok: true };
}
export async function reactivateTenantUser({
tenantId,
targetUserId,
}: {
tenantId: string;
targetUserId: string;
}): Promise<{ ok: true } | { ok: false; reason: "not-found" | "already-active" }> {
const member = await db.tenantUser.findFirst({
where: { tenantId, userId: targetUserId },
select: { id: true, status: true },
});
if (!member) {
return { ok: false, reason: "not-found" };
}
if (member.status === TenantUserStatus.ACTIVE) {
return { ok: false, reason: "already-active" };
}
await db.tenantUser.update({ where: { id: member.id }, data: { status: TenantUserStatus.ACTIVE } });
return { ok: true };
}