fix(professors, settings): add auto-migration for legacy professors, deep populate user in class/session/course services, and fix SMS bypass number persistence
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
// /components/professors/professorMigration.js
|
||||
'use strict';
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const Professor = require('./professorModel');
|
||||
const User = require('../users/userModel');
|
||||
const Role = require('../roles/roleModel');
|
||||
const logger = require('../../utils/logger');
|
||||
const { allocatePlaceholderNationalId } = require('../../utils/nationalId');
|
||||
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const ensureProfessorRole = async () => {
|
||||
let role = await Role.findOne({ name: 'Professor' });
|
||||
if (!role) {
|
||||
role = await Role.create({
|
||||
name: 'Professor',
|
||||
description: 'استاد و مدرس دورهها',
|
||||
permissions: ['professors:read', 'classes:read', 'sessions:read', 'attendances:read'],
|
||||
isSystem: true
|
||||
});
|
||||
}
|
||||
return role;
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically migrate and link all legacy Professor records to the Users table.
|
||||
* Ensures every Professor document has a valid, populated `user` reference.
|
||||
*/
|
||||
const migrateAndLinkProfessorsToUsers = async () => {
|
||||
try {
|
||||
const professorRole = await ensureProfessorRole();
|
||||
const professors = await Professor.find({}).setOptions({ _skipUserPopulate: true });
|
||||
|
||||
if (!professors.length) {
|
||||
logger.info('Professor migration: No professor records found.');
|
||||
return { total: 0, migrated: 0, alreadyLinked: 0 };
|
||||
}
|
||||
|
||||
let migratedCount = 0;
|
||||
let alreadyLinkedCount = 0;
|
||||
|
||||
for (const prof of professors) {
|
||||
const rawDoc = prof._doc || prof;
|
||||
let linkedUser = null;
|
||||
|
||||
// 1. Check if professor already has a valid user reference
|
||||
if (prof.user && mongoose.Types.ObjectId.isValid(prof.user)) {
|
||||
linkedUser = await User.findById(prof.user);
|
||||
}
|
||||
|
||||
if (linkedUser) {
|
||||
// Ensure user role is Professor if not an admin
|
||||
if (!linkedUser.role || (String(linkedUser.role) !== String(professorRole._id) && linkedUser.role?.name !== 'SuperAdmin' && linkedUser.role?.name !== 'Admin')) {
|
||||
linkedUser.role = professorRole._id;
|
||||
await linkedUser.save();
|
||||
}
|
||||
alreadyLinkedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Professor is not linked to a user. Find matching user by phone or national ID
|
||||
const rawPhone = String(rawDoc.phoneNumber || rawDoc.phone || '').trim();
|
||||
const rawNationalId = String(rawDoc.nationalIdCode || rawDoc.nationalId || '').trim();
|
||||
const rawName = String(rawDoc.name || '').trim();
|
||||
const rawSurname = String(rawDoc.surname || '').trim();
|
||||
const fullName = [rawName, rawSurname].filter(Boolean).join(' ').trim() || 'استاد بدون نام';
|
||||
|
||||
if (rawPhone) {
|
||||
linkedUser = await User.findOne({ phoneNumber: rawPhone });
|
||||
}
|
||||
if (!linkedUser && rawNationalId) {
|
||||
linkedUser = await User.findOne({ nationalIdCode: rawNationalId });
|
||||
}
|
||||
|
||||
// 3. If matching user found, link and update role
|
||||
if (linkedUser) {
|
||||
if (!linkedUser.role || (String(linkedUser.role) !== String(professorRole._id) && linkedUser.role?.name !== 'SuperAdmin' && linkedUser.role?.name !== 'Admin')) {
|
||||
linkedUser.role = professorRole._id;
|
||||
}
|
||||
if (!linkedUser.name && fullName) {
|
||||
linkedUser.name = fullName;
|
||||
}
|
||||
if (!linkedUser.cardNumber && rawDoc.cardNumber) {
|
||||
linkedUser.cardNumber = String(rawDoc.cardNumber).trim();
|
||||
}
|
||||
if (!linkedUser.shabaNumber && (rawDoc.shabaNumber || rawDoc.iban)) {
|
||||
linkedUser.shabaNumber = String(rawDoc.shabaNumber || rawDoc.iban).trim();
|
||||
}
|
||||
await linkedUser.save();
|
||||
|
||||
prof.user = linkedUser._id;
|
||||
await prof.save();
|
||||
migratedCount++;
|
||||
logger.info(`Professor migration: Linked professor "${fullName}" (${prof._id}) to existing user (${linkedUser._id}).`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. If no matching user found, create a new User for this professor
|
||||
let nationalIdCode = rawNationalId;
|
||||
if (!nationalIdCode) {
|
||||
nationalIdCode = await allocatePlaceholderNationalId(rawPhone || fullName);
|
||||
}
|
||||
|
||||
const generatedUsername = await generateUsername(fullName);
|
||||
const plainPassword = generateSimplePassword();
|
||||
const passwordHash = await bcrypt.hash(plainPassword, 10);
|
||||
|
||||
const newUser = await User.create({
|
||||
name: fullName,
|
||||
nationalIdCode,
|
||||
phoneNumber: rawPhone || undefined,
|
||||
email: rawDoc.email ? String(rawDoc.email).trim().toLowerCase() : undefined,
|
||||
role: professorRole._id,
|
||||
username: generatedUsername,
|
||||
passwordHash,
|
||||
cardNumber: rawDoc.cardNumber ? String(rawDoc.cardNumber).trim() : undefined,
|
||||
shabaNumber: (rawDoc.shabaNumber || rawDoc.iban) ? String(rawDoc.shabaNumber || rawDoc.iban).trim() : undefined,
|
||||
isActive: prof.isActive !== false
|
||||
});
|
||||
|
||||
prof.user = newUser._id;
|
||||
await prof.save();
|
||||
migratedCount++;
|
||||
logger.info(`Professor migration: Created new user (${newUser._id}) for legacy professor "${fullName}" (${prof._id}).`);
|
||||
}
|
||||
|
||||
logger.info(`Professor migration complete: ${professors.length} total, ${migratedCount} newly linked/created, ${alreadyLinkedCount} already linked.`);
|
||||
return { total: professors.length, migrated: migratedCount, alreadyLinked: alreadyLinkedCount };
|
||||
} catch (error) {
|
||||
logger.error('Error during migrateAndLinkProfessorsToUsers:', error);
|
||||
return { error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
migrateAndLinkProfessorsToUsers,
|
||||
ensureProfessorRole
|
||||
};
|
||||
Reference in New Issue
Block a user