97 lines
3.3 KiB
TypeScript
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 };
|
|
}
|