import { Prisma, Tenant, Role } from "@hivegrid/db/prisma-browser"; import { UserWithoutPassword, deleteUser } from "../db/users.db.server"; import { deleteAndCancelTenant } from "./tenantService"; import { db } from "../db.server"; import { invalidateUserSnapshot } from "../db/user-snapshot-cache.server"; import { getRlsExecutionContext, runWithIsolatedUserRlsContext, runWithRlsContext } from "@hivegrid/db/rls-context.server"; import EventsService from "~/modules/events/services/.server/EventsService"; import type { AccountDeletedDto } from "~/modules/events/dtos/AccountDeletedDto"; import type { UserProfileDeletedDto } from "~/modules/events/dtos/UserProfileDeletedDto"; import { invalidateUserPermissionCacheScopes } from "../db/permissions/permission-cache.server"; type DeleteUserTarget = Pick; export async function deleteUserWithItsTenants({ actorUserId, targetUser, request }: { actorUserId: string; targetUser: DeleteUserTarget; request?: Request }) { const userTenants = await runWithIsolatedUserRlsContext(actorUserId, () => db.tenant.findMany({ where: { users: { some: { userId: targetUser.id } } }, select: { id: true, name: true }, orderBy: { id: "asc" }, }) ); const deletedTenants: Tenant[] = []; for (const tenant of userTenants) { const deletedTenant = await runWithIsolatedUserRlsContext(actorUserId, () => runWithRlsContext({ tenantId: tenant.id }, async () => { const memberships = await db.tenantUser.findMany({ where: { tenantId: tenant.id }, select: { userId: true }, }); if (memberships.length !== 1 || memberships[0]?.userId !== targetUser.id) { return null; } const eventData = { tenant: { id: tenant.id, name: tenant.name }, user: { id: targetUser.id, email: targetUser.email }, } satisfies AccountDeletedDto; return deleteAndCancelTenant({ tenantId: tenant.id, userId: actorUserId, request, eventData, authorizeDeletion: async () => { const currentMemberships = await db.tenantUser.findMany({ where: { tenantId: tenant.id }, select: { userId: true }, }); return currentMemberships.length === 1 && currentMemberships[0]?.userId === targetUser.id; }, }); }) ); if (deletedTenant) { deletedTenants.push(deletedTenant); } } const deletedAccounts = deletedTenants.map( (tenant) => ({ tenant: { id: tenant.id, name: tenant.name }, user: { id: targetUser.id, email: targetUser.email }, }) satisfies AccountDeletedDto ); const deletedUser = await runWithIsolatedUserRlsContext(actorUserId, () => EventsService.createUserEventBeforeMutation( { request, event: "user.profile.deleted", userId: actorUserId, data: { id: targetUser.id, email: targetUser.email, firstName: targetUser.firstName, lastName: targetUser.lastName, deletedAccounts, } satisfies UserProfileDeletedDto, }, () => deleteUser(targetUser.id) ) ); return { deletedUser, deletedTenants, }; } export async function setUserRoles({ user, roles, type, }: { user: UserWithoutPassword; roles: { role: Role; tenantId: string | null }[]; type: "admin" | "app"; }) { const cacheScopes = await db.$transaction((transaction) => replaceUserRolesInTransaction(transaction, { userId: user.id, roles, type, }), ); await invalidateUserPermissionCacheScopes(cacheScopes); await invalidateUserSnapshot(user.id); return { count: roles.length }; } async function replaceUserRolesInTransaction( transaction: Prisma.TransactionClient, { userId, roles, type, }: { userId: string; roles: { role: Role; tenantId: string | null }[]; type: "admin" | "app"; }, ): Promise<{ userId: string; tenantId: string | null }[]> { const roleIds = [...new Set(roles.map(({ role }) => role.id))]; if (roleIds.length !== roles.length) { throw new Error("Duplicate role assignments are not allowed"); } const persistedRoles = await transaction.role.findMany({ where: { id: { in: roleIds } }, select: { id: true, tenantId: true, type: true }, }); if (persistedRoles.length !== roleIds.length) { throw new Error("One or more role assignments no longer exist"); } const currentTenantId = getRlsExecutionContext()?.tenantId ?? null; if (type === "admin") { throw new Error("Legacy UserRole-based admin assignment is disabled; use AdminUserRole"); } const persistedById = new Map(persistedRoles.map((role) => [role.id, role])); for (const assignment of roles) { const persisted = persistedById.get(assignment.role.id); if (!persisted || persisted.type !== type) { throw new Error("Role assignment type does not match the requested scope"); } if (!currentTenantId || assignment.tenantId !== currentTenantId || (persisted.tenantId !== null && persisted.tenantId !== currentTenantId)) { throw new Error("Tenant role assignments require the matching active tenant context"); } } const previousAssignments = await transaction.userRole.findMany({ where: { userId, role: { type } }, select: { tenantId: true }, }); await transaction.userRole.deleteMany({ where: { userId, role: { type } }, }); if (roles.length > 0) { await transaction.userRole.createMany({ data: roles.map(({ role, tenantId }) => ({ userId, roleId: role.id, tenantId })), }); } return [...new Set([...previousAssignments.map(({ tenantId }) => tenantId), ...roles.map(({ tenantId }) => tenantId)])].map((tenantId) => ({ userId, tenantId, })); }