This commit is contained in:
silverpro89
2026-01-19 20:32:23 +07:00
parent 70838a4bc1
commit 97e2e8402e
14 changed files with 10115 additions and 686 deletions

View File

@@ -0,0 +1,266 @@
/**
* 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();