/** * Role Helper Service - Tiện ích để làm việc với role và permissions */ const { UserProfile, Role, Permission, UserAssignment, School, Class } = require('../models'); const { Op } = require('sequelize'); class RoleHelperService { /** * Load role và permissions cho user (tự động chọn fast/flexible path) */ async getUserRoleAndPermissions(userId) { try { // Load profile const profile = await UserProfile.findOne({ where: { user_id: userId }, }); if (!profile) { return { roleInfo: null, permissions: [] }; } // Case 1: Fast Path - primary_role_info có sẵn if (profile.primary_role_info) { return await this._loadFromPrimaryRole(profile); } // Case 2: Flexible Path - check UserAssignment return await this._loadFromAssignments(userId); } catch (error) { console.error('[RoleHelper] Error loading role:', error); throw error; } } /** * Load từ primary_role_info (Fast Path) */ async _loadFromPrimaryRole(profile) { const roleInfo = profile.primary_role_info; let permissions = []; if (roleInfo.role_id) { const role = await Role.findByPk(roleInfo.role_id, { include: [{ model: Permission, as: 'permissions', through: { attributes: [] }, where: { is_active: true }, required: false, }], }); if (role) { permissions = role.permissions.map(p => ({ code: p.permission_code, name: p.permission_name, resource: p.resource, action: p.action, })); } } return { roleInfo, permissions }; } /** * Load từ UserAssignment (Flexible Path) */ async _loadFromAssignments(userId) { const assignments = await UserAssignment.findAll({ where: { user_id: userId, is_active: true, [Op.or]: [ { valid_until: null }, { valid_until: { [Op.gt]: new Date() } }, ], }, include: [ { model: Role, as: 'role', include: [{ model: Permission, as: 'permissions', through: { attributes: [] }, where: { is_active: true }, required: false, }], }, { model: School, as: 'school', attributes: ['id', 'school_name'], }, { model: Class, as: 'class', attributes: ['id', 'class_name'], }, ], order: [['is_primary', 'DESC']], }); if (assignments.length === 0) { return { roleInfo: null, permissions: [] }; } const primaryAssignment = assignments[0]; const roleInfo = { role_id: primaryAssignment.role?.id, role_code: primaryAssignment.role?.role_code, role_name: primaryAssignment.role?.role_name, school: primaryAssignment.school ? { id: primaryAssignment.school.id, name: primaryAssignment.school.school_name, } : null, class: primaryAssignment.class ? { id: primaryAssignment.class.id, name: primaryAssignment.class.class_name, } : null, assignments: assignments.map(a => ({ school: a.school ? { id: a.school.id, name: a.school.school_name } : null, class: a.class ? { id: a.class.id, name: a.class.class_name } : null, role: { id: a.role.id, code: a.role.role_code, name: a.role.role_name, }, })), }; // Merge permissions từ tất cả roles const permissionMap = new Map(); assignments.forEach(a => { a.role?.permissions?.forEach(p => { permissionMap.set(p.permission_code, { code: p.permission_code, name: p.permission_name, resource: p.resource, action: p.action, }); }); }); const permissions = Array.from(permissionMap.values()); return { roleInfo, permissions }; } /** * Check xem user có permission cụ thể không */ async hasPermission(userId, permissionCode) { const { permissions } = await this.getUserRoleAndPermissions(userId); return permissions.some(p => p.code === permissionCode); } /** * Check xem user có role cụ thể không */ async hasRole(userId, roleCode) { const { roleInfo } = await this.getUserRoleAndPermissions(userId); if (!roleInfo) return false; // Check primary role if (roleInfo.role_code === roleCode) return true; // Check assignments (nếu có) if (roleInfo.assignments) { return roleInfo.assignments.some(a => a.role.code === roleCode); } return false; } /** * Update primary_role_info cho student */ async updateStudentRoleInfo(userId, studentDetail) { const profile = await UserProfile.findOne({ where: { user_id: userId }, }); if (!profile) { throw new Error('User profile not found'); } const studentRole = await Role.findOne({ where: { role_code: 'student' }, }); if (!studentRole) { throw new Error('Student role not found'); } const roleInfo = { role_id: studentRole.id, role_code: studentRole.role_code, role_name: studentRole.role_name, school: studentDetail.school ? { id: studentDetail.school.id, name: studentDetail.school.school_name, } : null, class: studentDetail.class ? { id: studentDetail.class.id, name: studentDetail.class.class_name, } : null, grade: studentDetail.grade ? { id: studentDetail.grade.id, name: studentDetail.grade.grade_name, } : null, student_code: studentDetail.student_code, enrollment_date: studentDetail.enrollment_date, status: studentDetail.status, }; await profile.update({ primary_role_info: roleInfo }); // TODO: Invalidate Redis cache // await redis.del(`user:${userId}:permissions`); return roleInfo; } /** * Clear primary_role_info (chuyển sang dùng UserAssignment) */ async clearPrimaryRoleInfo(userId) { const profile = await UserProfile.findOne({ where: { user_id: userId }, }); if (!profile) { throw new Error('User profile not found'); } await profile.update({ primary_role_info: null }); // TODO: Invalidate Redis cache // await redis.del(`user:${userId}:permissions`); return true; } /** * Get statistics về fast/flexible path usage */ async getUsageStatistics() { const [results] = await UserProfile.sequelize.query(` SELECT CASE WHEN primary_role_info IS NOT NULL THEN 'Fast Path (primary_role_info)' ELSE 'Flexible Path (UserAssignment)' END as strategy, COUNT(*) as count, ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM user_profiles), 2) as percentage FROM user_profiles GROUP BY strategy `); return results; } } module.exports = new RoleHelperService();