Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/db/permissions/userRoles.db.server.ts
T

370 lines
10 KiB
TypeScript

import { Permission, Role, RolePermission, UserRole } from "@toolctrl/db/prisma-browser";
import { TenantUserType } from "~/application/enums/tenants/TenantUserType";
import { db } from "../../db.server";
import { cachified, clearCacheKey } from "~/utils/cache.server";
export type UserRoleWithPermission = UserRole & {
role: Role & { permissions: (RolePermission & { permission: Permission })[] };
};
export type UserRoleAssignment = {
userId: string;
roleId: string;
tenantId?: string | null;
};
function normalizeTenantId(tenantId: string | null | undefined): string | null {
return tenantId ?? null;
}
function getUserRoleAssignmentKey(assignment: UserRoleAssignment): string {
return `${assignment.userId}:${assignment.roleId}:${normalizeTenantId(assignment.tenantId)}`;
}
function clearUserRoleCache(userId: string, tenantId: string | null): void {
clearCacheKey(`userRoles:${userId}:${tenantId}`);
}
function isUniqueConstraintError(error: unknown): boolean {
return Boolean(error && typeof error === "object" && "code" in error && error.code === "P2002");
}
export async function getUserRole(userId: string, roleId: string, tenantId?: string | null) {
return await db.userRole.findFirst({
where: {
userId,
roleId,
tenantId,
},
});
}
export async function getUserRoleInTenant(userId: string, tenantId: string, roleName: string) {
return await db.userRole.findFirst({
where: {
userId,
role: {
name: roleName,
},
tenantId,
},
});
}
export async function getUserRoleInAdmin(userId: string, roleName: string) {
return await db.userRole.findFirst({
where: {
userId,
role: {
name: roleName,
},
tenantId: null,
},
});
}
export async function getUserRoles(userId: string, tenantId?: string | null): Promise<UserRoleWithPermission[]> {
return await db.userRole.findMany({
where: {
userId,
tenantId,
},
include: {
role: {
include: {
permissions: {
include: {
permission: true,
},
},
},
},
},
orderBy: {
role: {
order: "asc",
},
},
});
}
export async function getPermissionsByUser(userId: string, tenantId: string | null): Promise<string[]> {
const userRoles = await cachified({
key: `userRoles:${userId}:${tenantId}`,
ttl: 1000 * 60 * 60 * 24,
getFreshValue: () =>
db.userRole.findMany({
where: { userId, tenantId },
include: { role: { include: { permissions: { include: { permission: { select: { name: true } } } } } } },
}),
});
const roles: string[] = [];
const names: string[] = [];
userRoles.forEach((userRoles) => {
if (!roles.includes(userRoles.role.name)) {
roles.push(userRoles.role.name);
}
userRoles.role.permissions.forEach((permission) => {
if (!names.includes(permission.permission.name)) {
names.push(permission.permission.name);
}
});
});
// console.log({
// userId,
// tenantId,
// roles,
// permissions: names,
// });
return names;
}
export async function userHasPermission(userId: string, tenantId: string | null, permissionName: string): Promise<boolean> {
const permissions = await getPermissionsByUser(userId, tenantId);
return permissions.includes(permissionName);
}
export async function findPermissionByUser(persmissionName: string, userId: string, tenantId?: string | null): Promise<string | null> {
const userRoles = await db.userRole.findMany({
where: { userId, tenantId, role: { permissions: { some: { permission: { name: persmissionName } } } } },
include: { role: { include: { permissions: { include: { permission: { select: { name: true } } } } } } },
});
const roles: string[] = [];
const names: string[] = [];
userRoles.forEach((userRoles) => {
if (!roles.includes(userRoles.role.name)) {
roles.push(userRoles.role.name);
}
userRoles.role.permissions.forEach((permission) => {
if (!names.includes(permission.permission.name)) {
names.push(permission.permission.name);
}
});
});
// console.log({
// userId,
// tenantId,
// roles,
// permissions: names,
// });
return names.length > 0 ? names[0] : null;
}
export async function countUserPermission(userId: string, tenantId: string | null, permissionName: string): Promise<number> {
const normal = await db.permission.count({
where: {
name: permissionName,
inRoles: { some: { role: { users: { some: { userId, tenantId } } } } },
},
});
if (normal > 0) {
return normal;
}
return 0;
}
export async function findUserRolesByIds(userId: string, tenantId: string | null, roleIds: string[]): Promise<UserRole[]> {
return await db.userRole.findMany({
where: {
userId,
tenantId,
roleId: {
in: roleIds,
},
},
});
}
export async function getUsersRolesInTenant(tenantId: string) {
return await db.userRole.findMany({
where: {
tenantId,
},
include: {
role: true,
user: true,
},
});
}
export async function ensureTenantUserAppRoles({
userId,
tenantId,
tenantUserType,
}: {
userId: string;
tenantId: string;
tenantUserType: number;
}): Promise<{ created: number; roleCount: number }> {
const existingRoleCount = await db.userRole.count({
where: {
userId,
tenantId,
role: {
type: "app",
},
},
});
if (existingRoleCount > 0) {
return { created: 0, roleCount: existingRoleCount };
}
const shouldGrantAllAppRoles = tenantUserType === TenantUserType.OWNER || tenantUserType === TenantUserType.ADMIN;
const roles = await db.role.findMany({
where: {
type: "app",
OR: [{ tenantId: null }, { tenantId }],
...(shouldGrantAllAppRoles ? {} : { assignToNewUsers: true }),
},
orderBy: {
order: "asc",
},
});
const existingRoles = await findUserRolesByIds(
userId,
tenantId,
roles.map((role) => role.id)
);
const existingRoleIds = new Set(existingRoles.map((role) => role.roleId));
const missingRoles = roles.filter((role) => !existingRoleIds.has(role.id));
if (missingRoles.length > 0) {
await createUserRoles(
userId,
missingRoles.map((role) => ({ id: role.id, tenantId }))
);
} else {
clearCacheKey(`userRoles:${userId}:${tenantId}`);
}
return { created: missingRoles.length, roleCount: roles.length };
}
export async function createUserRole(userId: string, roleId: string, tenantId: string | null = null) {
const existing = await getUserRole(userId, roleId, tenantId);
if (existing) {
return existing;
}
try {
const item = await db.userRole.create({
data: {
userId,
roleId,
tenantId,
},
});
clearCacheKey(`userRoles:${userId}:${tenantId}`);
return item;
} catch (error) {
if (!isUniqueConstraintError(error)) {
throw error;
}
const createdByConcurrentRequest = await getUserRole(userId, roleId, tenantId);
if (!createdByConcurrentRequest) {
throw error;
}
return createdByConcurrentRequest;
}
}
/**
* Grants an admin-scoped RBAC role (e.g. "SuperAdmin") to a user by role name,
* idempotently. Used by the Admin-SSO callback to wire the JIT-provisioned
* `super_admin` identity to its concrete permission set. `createUserRole`
* invalidates the `userRoles:<userId>:null` cache, so the granted permissions
* are visible on the very next request — independent of the cache TTL.
*/
export async function ensureAdminUserRoleByName(userId: string, roleName: string): Promise<void> {
const role = await db.role.findFirst({
where: { name: roleName, type: "admin" },
select: { id: true },
});
if (!role) {
return;
}
await createUserRole(userId, role.id, null);
}
export async function createUserRoles(userId: string, roles: { id: string; tenantId: string | null }[]) {
return await createUserRoleAssignments(roles.map((role) => ({ userId, roleId: role.id, tenantId: role.tenantId })));
}
export async function createUserRoleAssignments(assignments: UserRoleAssignment[]) {
const uniqueAssignments = [...new Map(assignments.map((assignment) => [getUserRoleAssignmentKey(assignment), assignment])).values()].map((assignment) => ({
userId: assignment.userId,
roleId: assignment.roleId,
tenantId: normalizeTenantId(assignment.tenantId),
}));
const cacheScopes = [...new Map(uniqueAssignments.map((assignment) => [`${assignment.userId}:${assignment.tenantId}`, assignment])).values()];
if (uniqueAssignments.length === 0) {
return { count: 0 };
}
const userIds = [...new Set(uniqueAssignments.map((assignment) => assignment.userId))];
const roleIds = [...new Set(uniqueAssignments.map((assignment) => assignment.roleId))];
const tenantIds = [...new Set(uniqueAssignments.map((assignment) => assignment.tenantId))];
const nonNullTenantIds = tenantIds.filter((tenantId): tenantId is string => tenantId !== null);
const tenantWhere =
tenantIds.includes(null) && nonNullTenantIds.length > 0
? { OR: [{ tenantId: null }, { tenantId: { in: nonNullTenantIds } }] }
: tenantIds.includes(null)
? { tenantId: null }
: { tenantId: { in: nonNullTenantIds } };
const existingAssignments = await db.userRole.findMany({
where: {
userId: { in: userIds },
roleId: { in: roleIds },
...tenantWhere,
},
select: {
userId: true,
roleId: true,
tenantId: true,
},
});
const existingAssignmentKeys = new Set(existingAssignments.map((assignment) => getUserRoleAssignmentKey(assignment)));
const missingAssignments = uniqueAssignments.filter((assignment) => !existingAssignmentKeys.has(getUserRoleAssignmentKey(assignment)));
const result =
missingAssignments.length > 0
? await db.userRole.createMany({
data: missingAssignments,
skipDuplicates: true,
})
: { count: 0 };
cacheScopes.forEach((scope) => {
clearUserRoleCache(scope.userId, scope.tenantId);
});
return result;
}
export async function deleteUserRole(userId: string, roleId: string, tenantId: string | null = null) {
return await db.userRole
.deleteMany({
where: {
userId,
roleId,
tenantId,
},
})
.then((item) => {
clearCacheKey(`userRoles:${userId}:${tenantId}`);
return item;
});
}
export async function deleteAllUserRoles(userId: string, type: string) {
return await db.userRole.deleteMany({
where: {
userId,
role: {
type,
},
},
});
}