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

@@ -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();

View File

@@ -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;

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();

View File

@@ -0,0 +1,35 @@
import { Response } from 'express';
import prisma from '../../config/database';
import { AuthRequest } from '../../shared/middleware/auth';
import { ResponseFormatter } from '../../shared/utils/responseFormatter';
class DashboardController {
async getStats(req: AuthRequest, res: Response) {
const userId = req.user!.id;
const [contactsCount, activeTasksCount, unreadNotificationsCount] = await Promise.all([
prisma.contact.count(),
prisma.task.count({
where: {
status: { notIn: ['COMPLETED', 'CANCELLED'] },
},
}),
prisma.notification.count({
where: {
userId,
isRead: false,
},
}),
]);
res.json(
ResponseFormatter.success({
contacts: contactsCount,
activeTasks: activeTasksCount,
notifications: unreadNotificationsCount,
})
);
}
}
export default new DashboardController();

View File

@@ -0,0 +1,9 @@
import { Router } from 'express';
import { authenticate } from '../../shared/middleware/auth';
import dashboardController from './dashboard.controller';
const router = Router();
router.get('/stats', authenticate, dashboardController.getStats.bind(dashboardController));
export default router;

View File

@@ -19,15 +19,21 @@ export class HRController {
async findAllEmployees(req: AuthRequest, res: Response, next: NextFunction) {
try {
const page = parseInt(req.query.page as string) || 1;
const pageSize = parseInt(req.query.pageSize as string) || 20;
const filters = {
search: req.query.search,
departmentId: req.query.departmentId,
status: req.query.status,
};
const rawPage = parseInt(req.query.page as string, 10);
const rawPageSize = parseInt(req.query.pageSize as string, 10);
const page = Number.isNaN(rawPage) || rawPage < 1 ? 1 : rawPage;
const pageSize = Number.isNaN(rawPageSize) || rawPageSize < 1 || rawPageSize > 100 ? 20 : rawPageSize;
const rawSearch = req.query.search as string;
const rawDepartmentId = req.query.departmentId as string;
const rawStatus = req.query.status as string;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const filters: Record<string, string | undefined> = {};
if (rawSearch && typeof rawSearch === 'string' && rawSearch.trim()) filters.search = rawSearch.trim();
if (rawDepartmentId && uuidRegex.test(rawDepartmentId)) filters.departmentId = rawDepartmentId;
if (rawStatus && rawStatus !== 'all' && rawStatus.trim()) filters.status = rawStatus;
const result = await hrService.findAllEmployees(filters, page, pageSize);
res.json(ResponseFormatter.paginated(result.employees, result.total, result.page, result.pageSize));
} catch (error) {
@@ -135,6 +141,42 @@ export class HRController {
}
}
async getDepartmentsHierarchy(req: AuthRequest, res: Response, next: NextFunction) {
try {
const tree = await hrService.getDepartmentsHierarchy();
res.json(ResponseFormatter.success(tree));
} catch (error) {
next(error);
}
}
async createDepartment(req: AuthRequest, res: Response, next: NextFunction) {
try {
const department = await hrService.createDepartment(req.body, req.user!.id);
res.status(201).json(ResponseFormatter.success(department, 'تم إضافة القسم بنجاح - Department created'));
} catch (error) {
next(error);
}
}
async updateDepartment(req: AuthRequest, res: Response, next: NextFunction) {
try {
const department = await hrService.updateDepartment(req.params.id, req.body, req.user!.id);
res.json(ResponseFormatter.success(department, 'تم تحديث القسم - Department updated'));
} catch (error) {
next(error);
}
}
async deleteDepartment(req: AuthRequest, res: Response, next: NextFunction) {
try {
await hrService.deleteDepartment(req.params.id, req.user!.id);
res.json(ResponseFormatter.success({ success: true }, 'تم حذف القسم - Department deleted'));
} catch (error) {
next(error);
}
}
// ========== POSITIONS ==========
async findAllPositions(req: AuthRequest, res: Response, next: NextFunction) {

View File

@@ -32,6 +32,10 @@ router.post('/salaries/process', authorize('hr', 'salaries', 'process'), hrContr
// ========== DEPARTMENTS ==========
router.get('/departments', authorize('hr', 'all', 'read'), hrController.findAllDepartments);
router.get('/departments/hierarchy', authorize('hr', 'all', 'read'), hrController.getDepartmentsHierarchy);
router.post('/departments', authorize('hr', 'all', 'create'), hrController.createDepartment);
router.put('/departments/:id', authorize('hr', 'all', 'update'), hrController.updateDepartment);
router.delete('/departments/:id', authorize('hr', 'all', 'delete'), hrController.deleteDepartment);
// ========== POSITIONS ==========

View File

@@ -5,14 +5,52 @@ import { AuditLogger } from '../../shared/utils/auditLogger';
class HRService {
// ========== EMPLOYEES ==========
private normalizeEmployeeData(data: any): Record<string, any> {
const toStr = (v: any) => (v != null && String(v).trim()) ? String(v).trim() : undefined;
const toDate = (v: any) => {
if (!v || !String(v).trim()) return undefined;
const d = new Date(v);
return isNaN(d.getTime()) ? undefined : d;
};
const toNum = (v: any) => (v != null && v !== '') ? Number(v) : undefined;
const raw: Record<string, any> = {
firstName: toStr(data.firstName),
lastName: toStr(data.lastName),
firstNameAr: toStr(data.firstNameAr),
lastNameAr: toStr(data.lastNameAr),
email: toStr(data.email),
phone: toStr(data.phone),
mobile: toStr(data.mobile),
dateOfBirth: toDate(data.dateOfBirth),
gender: toStr(data.gender),
nationality: toStr(data.nationality),
nationalId: toStr(data.nationalId),
employmentType: toStr(data.employmentType),
contractType: toStr(data.contractType),
hireDate: toDate(data.hireDate),
departmentId: toStr(data.departmentId),
positionId: toStr(data.positionId),
reportingToId: toStr(data.reportingToId) || undefined,
basicSalary: toNum(data.baseSalary ?? data.basicSalary) ?? 0,
};
return Object.fromEntries(Object.entries(raw).filter(([, v]) => v !== undefined));
}
async createEmployee(data: any, userId: string) {
const uniqueEmployeeId = await this.generateEmployeeId();
const payload = this.normalizeEmployeeData(data);
if (!payload.firstName || !payload.lastName || !payload.email || !payload.mobile ||
!payload.hireDate || !payload.departmentId || !payload.positionId) {
throw new AppError(400, 'بيانات غير مكتملة - Missing required fields: firstName, lastName, email, mobile, hireDate, departmentId, positionId');
}
const employee = await prisma.employee.create({
data: {
uniqueEmployeeId,
...data,
},
...payload,
} as any,
include: {
department: true,
position: true,
@@ -132,9 +170,10 @@ class HRService {
throw new AppError(404, 'الموظف غير موجود - Employee not found');
}
const payload = this.normalizeEmployeeData(data);
const employee = await prisma.employee.update({
where: { id },
data,
data: payload,
include: {
department: true,
position: true,
@@ -383,11 +422,112 @@ class HRService {
async findAllDepartments() {
const departments = await prisma.department.findMany({
where: { isActive: true },
include: {
parent: { select: { id: true, name: true, nameAr: true } },
_count: { select: { children: true, employees: true } }
},
orderBy: { name: 'asc' }
});
return departments;
}
async getDepartmentsHierarchy() {
const departments = await prisma.department.findMany({
where: { isActive: true },
include: {
parent: { select: { id: true, name: true, nameAr: true } },
employees: {
where: { status: 'ACTIVE' },
select: { id: true, firstName: true, lastName: true, firstNameAr: true, lastNameAr: true, position: { select: { title: true, titleAr: true } } }
},
positions: { select: { id: true, title: true, titleAr: true } },
_count: { select: { children: true, employees: true } }
},
orderBy: { name: 'asc' }
});
const buildTree = (parentId: string | null): any[] =>
departments
.filter((d) => d.parentId === parentId)
.map((d) => ({
id: d.id,
name: d.name,
nameAr: d.nameAr,
code: d.code,
parentId: d.parentId,
description: d.description,
employees: d.employees,
positions: d.positions,
_count: d._count,
children: buildTree(d.id)
}));
return buildTree(null);
}
async createDepartment(data: { name: string; nameAr?: string; code: string; parentId?: string; description?: string }, userId: string) {
const existing = await prisma.department.findUnique({ where: { code: data.code } });
if (existing) {
throw new AppError(400, 'كود القسم مستخدم مسبقاً - Department code already exists');
}
if (data.parentId) {
const parent = await prisma.department.findUnique({ where: { id: data.parentId } });
if (!parent) {
throw new AppError(400, 'القسم الأب غير موجود - Parent department not found');
}
}
const department = await prisma.department.create({
data: {
name: data.name,
nameAr: data.nameAr,
code: data.code,
parentId: data.parentId || null,
description: data.description
}
});
await AuditLogger.log({ entityType: 'DEPARTMENT', entityId: department.id, action: 'CREATE', userId, changes: { created: department } });
return department;
}
async updateDepartment(id: string, data: { name?: string; nameAr?: string; code?: string; parentId?: string; description?: string; isActive?: boolean }, userId: string) {
const existing = await prisma.department.findUnique({ where: { id } });
if (!existing) {
throw new AppError(404, 'القسم غير موجود - Department not found');
}
if (data.code && data.code !== existing.code) {
const duplicate = await prisma.department.findUnique({ where: { code: data.code } });
if (duplicate) {
throw new AppError(400, 'كود القسم مستخدم مسبقاً - Department code already exists');
}
}
if (data.parentId === id) {
throw new AppError(400, 'لا يمكن تعيين القسم كأب لنفسه - Department cannot be its own parent');
}
const department = await prisma.department.update({
where: { id },
data: { name: data.name, nameAr: data.nameAr, code: data.code, parentId: data.parentId ?? undefined, description: data.description, isActive: data.isActive }
});
await AuditLogger.log({ entityType: 'DEPARTMENT', entityId: id, action: 'UPDATE', userId, changes: { before: existing, after: department } });
return department;
}
async deleteDepartment(id: string, userId: string) {
const dept = await prisma.department.findUnique({
where: { id },
include: { _count: { select: { children: true, employees: true } } }
});
if (!dept) {
throw new AppError(404, 'القسم غير موجود - Department not found');
}
if (dept._count.children > 0) {
throw new AppError(400, 'لا يمكن حذف قسم يحتوي على أقسام فرعية - Cannot delete department with sub-departments');
}
if (dept._count.employees > 0) {
throw new AppError(400, 'لا يمكن حذف قسم فيه موظفون - Cannot delete department with employees');
}
await prisma.department.delete({ where: { id } });
await AuditLogger.log({ entityType: 'DEPARTMENT', entityId: id, action: 'DELETE', userId });
return { success: true };
}
// ========== POSITIONS ==========
async findAllPositions() {

View File

@@ -3,6 +3,7 @@ import adminRoutes from '../modules/admin/admin.routes';
import authRoutes from '../modules/auth/auth.routes';
import contactsRoutes from '../modules/contacts/contacts.routes';
import crmRoutes from '../modules/crm/crm.routes';
import dashboardRoutes from '../modules/dashboard/dashboard.routes';
import hrRoutes from '../modules/hr/hr.routes';
import inventoryRoutes from '../modules/inventory/inventory.routes';
import projectsRoutes from '../modules/projects/projects.routes';
@@ -12,6 +13,7 @@ const router = Router();
// Module routes
router.use('/admin', adminRoutes);
router.use('/dashboard', dashboardRoutes);
router.use('/auth', authRoutes);
router.use('/contacts', contactsRoutes);
router.use('/crm', crmRoutes);

View File

@@ -4,12 +4,47 @@ import { config } from '../../config';
import { AppError } from './errorHandler';
import prisma from '../../config/database';
export interface EffectivePermission {
module: string;
resource: string;
actions: string[];
}
function mergePermissions(
positionPerms: { module: string; resource: string; actions: unknown }[],
rolePerms: { module: string; resource: string; actions: unknown }[]
): EffectivePermission[] {
const key = (m: string, r: string) => `${m}:${r}`;
const map = new Map<string, Set<string>>();
const add = (m: string, r: string, actions: unknown) => {
const arr = Array.isArray(actions) ? actions : [];
const actionSet = new Set<string>(arr.map(String));
const k = key(m, r);
const existing = map.get(k);
if (existing) {
actionSet.forEach((a) => existing.add(a));
} else {
map.set(k, actionSet);
}
};
(positionPerms || []).forEach((p) => add(p.module, p.resource, p.actions));
(rolePerms || []).forEach((p) => add(p.module, p.resource, p.actions));
return Array.from(map.entries()).map(([k, actions]) => {
const [module, resource] = k.split(':');
return { module, resource, actions: Array.from(actions) };
});
}
export interface AuthRequest extends Request {
user?: {
id: string;
email: string;
employeeId?: string;
employee?: any;
effectivePermissions?: EffectivePermission[];
};
}
@@ -33,7 +68,7 @@ export const authenticate = async (
email: string;
};
// Get user with employee info
// Get user with employee + roles (Phase 3: multi-group)
const user = await prisma.user.findUnique({
where: { id: decoded.id },
include: {
@@ -47,6 +82,14 @@ export const authenticate = async (
department: true,
},
},
userRoles: {
where: { role: { isActive: true } },
include: {
role: {
include: { permissions: true },
},
},
},
},
});
@@ -59,12 +102,18 @@ export const authenticate = async (
throw new AppError(403, 'الوصول مرفوض - Access denied. Active employee record required.');
}
// Attach user to request
const positionPerms = user.employee?.position?.permissions ?? [];
const rolePerms = (user as any).userRoles?.flatMap(
(ur: any) => ur.role?.permissions ?? []
) ?? [];
const effectivePermissions = mergePermissions(positionPerms, rolePerms);
req.user = {
id: user.id,
email: user.email,
employeeId: user.employeeId || undefined,
employee: user.employee,
effectivePermissions,
};
next();
@@ -76,25 +125,24 @@ export const authenticate = async (
}
};
// Permission checking middleware
// Permission checking middleware (Position + Role permissions merged)
export const authorize = (module: string, resource: string, action: string) => {
return async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
if (!req.user?.employee?.position?.permissions) {
const perms = req.user?.effectivePermissions;
if (!perms || perms.length === 0) {
throw new AppError(403, 'الوصول مرفوض - Access denied');
}
// Find permission for this module and resource (check exact match or wildcard)
const permission = req.user.employee.position.permissions.find(
(p: any) => p.module === module && (p.resource === resource || p.resource === '*' || p.resource === 'all')
const permission = perms.find(
(p) => p.module === module && (p.resource === resource || p.resource === '*' || p.resource === 'all')
);
if (!permission) {
throw new AppError(403, 'الوصول مرفوض - Access denied');
}
// Check if action is allowed (check exact match or wildcard)
const actions = permission.actions as string[];
const actions = permission.actions;
if (!actions.includes(action) && !actions.includes('*') && !actions.includes('all')) {
throw new AppError(403, 'الوصول مرفوض - Access denied');
}

View File

@@ -40,10 +40,12 @@ export const errorHandler = (
// Handle validation errors
if (err instanceof Prisma.PrismaClientValidationError) {
const detail = process.env.NODE_ENV !== 'production' ? err.message : undefined;
return res.status(400).json({
success: false,
message: 'بيانات غير صالحة - Invalid data',
error: 'VALIDATION_ERROR',
...(detail && { detail }),
});
}