Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/services/userService.ts
T

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,
}));
}