RBAC: Phase 1-3, Total Salary fix, employee creation fix, permission groups, backup script

Made-with: Cursor
This commit is contained in:
Talal Sharabi
2026-03-04 19:31:08 +04:00
parent 6034f774ed
commit 8edeaf10f5
46 changed files with 2751 additions and 598 deletions

View File

@@ -406,6 +406,102 @@ class AdminService {
return withUserCount;
}
async createPosition(data: {
title: string;
titleAr?: string;
code: string;
departmentId: string;
level?: number;
description?: string;
isActive?: boolean;
}) {
const existing = await prisma.position.findUnique({
where: { code: data.code },
});
if (existing) {
throw new AppError(400, 'كود الدور مستخدم - Position code already exists');
}
const dept = await prisma.department.findUnique({
where: { id: data.departmentId },
});
if (!dept) {
throw new AppError(400, 'القسم غير موجود - Department not found');
}
return prisma.position.create({
data: {
title: data.title,
titleAr: data.titleAr,
code: data.code.trim().toUpperCase().replace(/\s+/g, '_'),
departmentId: data.departmentId,
level: data.level ?? 5,
description: data.description,
isActive: data.isActive ?? true,
},
include: {
department: { select: { name: true, nameAr: true } },
permissions: true,
},
});
}
async updatePosition(
positionId: string,
data: {
title?: string;
titleAr?: string;
code?: string;
departmentId?: string;
level?: number;
description?: string;
isActive?: boolean;
}
) {
const position = await prisma.position.findUnique({
where: { id: positionId },
});
if (!position) {
throw new AppError(404, 'الدور غير موجود - Position not found');
}
if (data.code && data.code !== position.code) {
const existing = await prisma.position.findUnique({
where: { code: data.code },
});
if (existing) {
throw new AppError(400, 'كود الدور مستخدم - Position code already exists');
}
}
if (data.departmentId && data.departmentId !== position.departmentId) {
const dept = await prisma.department.findUnique({
where: { id: data.departmentId },
});
if (!dept) {
throw new AppError(400, 'القسم غير موجود - Department not found');
}
}
const updateData: Record<string, any> = {};
if (data.title !== undefined) updateData.title = data.title;
if (data.titleAr !== undefined) updateData.titleAr = data.titleAr;
if (data.code !== undefined) updateData.code = data.code.trim().toUpperCase().replace(/\s+/g, '_');
if (data.departmentId !== undefined) updateData.departmentId = data.departmentId;
if (data.level !== undefined) updateData.level = data.level;
if (data.description !== undefined) updateData.description = data.description;
if (data.isActive !== undefined) updateData.isActive = data.isActive;
return prisma.position.update({
where: { id: positionId },
data: updateData,
include: {
department: { select: { name: true, nameAr: true } },
permissions: true,
},
});
}
async updatePositionPermissions(positionId: string, permissions: Array<{ module: string; resource: string; actions: string[] }>) {
const position = await prisma.position.findUnique({ where: { id: positionId } });
if (!position) {
@@ -429,6 +525,116 @@ class AdminService {
return this.getPositions().then((pos) => pos.find((p) => p.id === positionId) || position);
}
// ========== PERMISSION GROUPS (Phase 3 - optional roles for multi-group) ==========
async getPermissionGroups() {
return prisma.role.findMany({
where: { isActive: true },
include: {
permissions: true,
_count: { select: { userRoles: true } },
},
orderBy: { name: 'asc' },
});
}
async createPermissionGroup(data: { name: string; nameAr?: string; description?: string }) {
const existing = await prisma.role.findUnique({ where: { name: data.name } });
if (existing) {
throw new AppError(400, 'اسم المجموعة مستخدم - Group name already exists');
}
return prisma.role.create({
data: {
name: data.name,
nameAr: data.nameAr,
description: data.description,
},
include: { permissions: true },
});
}
async updatePermissionGroup(
id: string,
data: { name?: string; nameAr?: string; description?: string; isActive?: boolean }
) {
const role = await prisma.role.findUnique({ where: { id } });
if (!role) {
throw new AppError(404, 'المجموعة غير موجودة - Group not found');
}
if (data.name && data.name !== role.name) {
const existing = await prisma.role.findUnique({ where: { name: data.name } });
if (existing) {
throw new AppError(400, 'اسم المجموعة مستخدم - Group name already exists');
}
}
return prisma.role.update({
where: { id },
data,
include: { permissions: true },
});
}
async updatePermissionGroupPermissions(
roleId: string,
permissions: Array<{ module: string; resource: string; actions: string[] }>
) {
await prisma.rolePermission.deleteMany({ where: { roleId } });
if (permissions.length > 0) {
await prisma.rolePermission.createMany({
data: permissions.map((p) => ({
roleId,
module: p.module,
resource: p.resource,
actions: p.actions,
})),
});
}
return prisma.role.findUnique({
where: { id: roleId },
include: { permissions: true },
});
}
async getUserRoles(userId: string) {
return prisma.userRole.findMany({
where: { userId },
include: {
role: { include: { permissions: true } },
},
});
}
async assignUserRole(userId: string, roleId: string) {
const [user, role] = await Promise.all([
prisma.user.findUnique({ where: { id: userId } }),
prisma.role.findFirst({ where: { id: roleId, isActive: true } }),
]);
if (!user) throw new AppError(404, 'المستخدم غير موجود - User not found');
if (!role) throw new AppError(404, 'المجموعة غير موجودة - Group not found');
const existing = await prisma.userRole.findUnique({
where: { userId_roleId: { userId, roleId } },
});
if (existing) {
throw new AppError(400, 'المستخدم منتمي بالفعل لهذه المجموعة - User already in group');
}
return prisma.userRole.create({
data: { userId, roleId },
include: { role: true },
});
}
async removeUserRole(userId: string, roleId: string) {
const deleted = await prisma.userRole.deleteMany({
where: { userId, roleId },
});
if (deleted.count === 0) {
throw new AppError(404, 'لم يتم العثور على الانتماء - User not in group');
}
return { success: true };
}
}
export const adminService = new AdminService();