168 lines
5.8 KiB
TypeScript
168 lines
5.8 KiB
TypeScript
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<UserWithoutPassword, "id" | "email" | "firstName" | "lastName">;
|
|
|
|
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,
|
|
}));
|
|
}
|