RBAC: Phase 1-3, Total Salary fix, employee creation fix, permission groups, backup script
Made-with: Cursor
This commit is contained in:
@@ -134,6 +134,40 @@ class AdminController {
|
||||
}
|
||||
}
|
||||
|
||||
async createPosition(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const position = await adminService.createPosition({
|
||||
title: req.body.title,
|
||||
titleAr: req.body.titleAr,
|
||||
code: req.body.code,
|
||||
departmentId: req.body.departmentId,
|
||||
level: req.body.level,
|
||||
description: req.body.description,
|
||||
isActive: req.body.isActive,
|
||||
});
|
||||
res.status(201).json(ResponseFormatter.success(position));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async updatePosition(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const position = await adminService.updatePosition(req.params.id, {
|
||||
title: req.body.title,
|
||||
titleAr: req.body.titleAr,
|
||||
code: req.body.code,
|
||||
departmentId: req.body.departmentId,
|
||||
level: req.body.level,
|
||||
description: req.body.description,
|
||||
isActive: req.body.isActive,
|
||||
});
|
||||
res.json(ResponseFormatter.success(position));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async updatePositionPermissions(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const position = await adminService.updatePositionPermissions(
|
||||
@@ -145,6 +179,74 @@ class AdminController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== PERMISSION GROUPS (Phase 3) ==========
|
||||
|
||||
async getPermissionGroups(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const groups = await adminService.getPermissionGroups();
|
||||
res.json(ResponseFormatter.success(groups));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async createPermissionGroup(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const group = await adminService.createPermissionGroup(req.body);
|
||||
res.status(201).json(ResponseFormatter.success(group));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async updatePermissionGroup(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const group = await adminService.updatePermissionGroup(req.params.id, req.body);
|
||||
res.json(ResponseFormatter.success(group));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async updatePermissionGroupPermissions(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const group = await adminService.updatePermissionGroupPermissions(
|
||||
req.params.id,
|
||||
req.body.permissions
|
||||
);
|
||||
res.json(ResponseFormatter.success(group));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getUserRoles(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const roles = await adminService.getUserRoles(req.params.userId);
|
||||
res.json(ResponseFormatter.success(roles));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async assignUserRole(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const userRole = await adminService.assignUserRole(req.params.userId, req.body.roleId);
|
||||
res.status(201).json(ResponseFormatter.success(userRole));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async removeUserRole(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await adminService.removeUserRole(req.params.userId, req.params.roleId);
|
||||
res.json(ResponseFormatter.success({ success: true }));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const adminController = new AdminController();
|
||||
|
||||
@@ -89,6 +89,33 @@ router.get(
|
||||
adminController.getPositions
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/positions',
|
||||
authorize('admin', 'roles', 'create'),
|
||||
[
|
||||
body('title').notEmpty().trim(),
|
||||
body('code').notEmpty().trim(),
|
||||
body('departmentId').isUUID(),
|
||||
body('level').optional().isInt({ min: 1 }),
|
||||
],
|
||||
validate,
|
||||
adminController.createPosition
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/positions/:id',
|
||||
authorize('admin', 'roles', 'update'),
|
||||
[
|
||||
param('id').isUUID(),
|
||||
body('title').optional().notEmpty().trim(),
|
||||
body('code').optional().notEmpty().trim(),
|
||||
body('departmentId').optional().isUUID(),
|
||||
body('level').optional().isInt({ min: 1 }),
|
||||
],
|
||||
validate,
|
||||
adminController.updatePosition
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/positions/:id/permissions',
|
||||
authorize('admin', 'roles', 'update'),
|
||||
@@ -100,4 +127,68 @@ router.put(
|
||||
adminController.updatePositionPermissions
|
||||
);
|
||||
|
||||
// ========== PERMISSION GROUPS (Phase 3 - multi-group) ==========
|
||||
|
||||
router.get(
|
||||
'/permission-groups',
|
||||
authorize('admin', 'roles', 'read'),
|
||||
adminController.getPermissionGroups
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/permission-groups',
|
||||
authorize('admin', 'roles', 'create'),
|
||||
[
|
||||
body('name').notEmpty().trim(),
|
||||
],
|
||||
validate,
|
||||
adminController.createPermissionGroup
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/permission-groups/:id',
|
||||
authorize('admin', 'roles', 'update'),
|
||||
[param('id').isUUID()],
|
||||
validate,
|
||||
adminController.updatePermissionGroup
|
||||
);
|
||||
|
||||
router.put(
|
||||
'/permission-groups/:id/permissions',
|
||||
authorize('admin', 'roles', 'update'),
|
||||
[
|
||||
param('id').isUUID(),
|
||||
body('permissions').isArray(),
|
||||
],
|
||||
validate,
|
||||
adminController.updatePermissionGroupPermissions
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/users/:userId/roles',
|
||||
authorize('admin', 'users', 'read'),
|
||||
[param('userId').isUUID()],
|
||||
validate,
|
||||
adminController.getUserRoles
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/users/:userId/roles',
|
||||
authorize('admin', 'users', 'update'),
|
||||
[
|
||||
param('userId').isUUID(),
|
||||
body('roleId').isUUID(),
|
||||
],
|
||||
validate,
|
||||
adminController.assignUserRole
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/users/:userId/roles/:roleId',
|
||||
authorize('admin', 'users', 'update'),
|
||||
[param('userId').isUUID(), param('roleId').isUUID()],
|
||||
validate,
|
||||
adminController.removeUserRole
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user