370 lines
10 KiB
TypeScript
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,
|
|
},
|
|
},
|
|
});
|
|
}
|