feat: add append-only course/user data import and expand user profile
Support uploading structured JSON to create courses, classes, and students without wiping existing data, and merge user name fields while adding gender and registration details from source sheets.
This commit is contained in:
@@ -79,6 +79,7 @@ app.get('/api/health', (req, res) => {
|
||||
|
||||
const seedDatabase = require('./seed');
|
||||
const seedRoutes = require('./components/seed/seedRoutes');
|
||||
const dataImportRoutes = require('./components/dataImport/dataImportRoutes');
|
||||
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/users', userRoutes);
|
||||
@@ -95,6 +96,7 @@ app.use('/api/dashboard', dashboardRoutes);
|
||||
app.use('/api/activity-logs', activityLogRoutes);
|
||||
app.use('/api/contact-inquiries', contactInquiryRoutes);
|
||||
app.use('/api/seed', seedRoutes);
|
||||
app.use('/api/data-import', dataImportRoutes);
|
||||
|
||||
// ── Error Handlers ────────────────────────────────────────────────────────────
|
||||
app.use(notFoundHandler);
|
||||
|
||||
@@ -121,7 +121,7 @@ const getAllActivityLogs = async (queryParams = {}) => {
|
||||
|
||||
const [logs, totalCount] = await Promise.all([
|
||||
ActivityLog.find(filter)
|
||||
.populate('actor', 'name surname username')
|
||||
.populate('actor', 'name username')
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
.limit(limit)
|
||||
|
||||
@@ -39,7 +39,7 @@ const createCertificate = async (data) => {
|
||||
|
||||
const getCertificateById = async (id) => {
|
||||
const certificate = await Certificate.findById(id)
|
||||
.populate('user', 'name surname username nationalIdCode')
|
||||
.populate('user', 'name username nationalIdCode')
|
||||
.populate('course', 'title type');
|
||||
if (!certificate) {
|
||||
throw new AppError('CERTIFICATE_NOT_FOUND');
|
||||
@@ -56,7 +56,7 @@ const getAllCertificates = async (queryParams) => {
|
||||
|
||||
const [certificates, totalCount] = await Promise.all([
|
||||
Certificate.find(filter)
|
||||
.populate('user', 'name surname username')
|
||||
.populate('user', 'name username')
|
||||
.populate('course', 'title')
|
||||
.sort(sort)
|
||||
.skip(skip)
|
||||
|
||||
@@ -32,7 +32,7 @@ const getOne = async (id) => {
|
||||
const cls = await Class.findById(id)
|
||||
.populate({ path: 'course', select: 'title type price' })
|
||||
.populate({ path: 'professor', select: 'name surname phoneNumber' })
|
||||
.populate({ path: 'students', select: 'name surname phoneNumber' })
|
||||
.populate({ path: 'students', select: 'name phoneNumber gender' })
|
||||
.lean();
|
||||
if (!cls) throw new AppError('CLASS_NOT_FOUND');
|
||||
return cls;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// /components/dataImport/dataImportController.js
|
||||
'use strict';
|
||||
|
||||
const catchAsync = require('../../utils/catchAsync');
|
||||
const dataImportService = require('./dataImportService');
|
||||
const AppError = require('../../utils/AppError');
|
||||
const { successResponse } = require('../../utils/apiResponse');
|
||||
|
||||
exports.importJson = catchAsync(async (req, res) => {
|
||||
let payload = req.body;
|
||||
|
||||
if (req.file?.buffer) {
|
||||
try {
|
||||
payload = JSON.parse(req.file.buffer.toString('utf8'));
|
||||
} catch {
|
||||
throw new AppError('VALIDATION_FAILED', null, 'Uploaded file is not valid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
// Allow { data: { courses: [...] } } wrappers
|
||||
if (payload?.data?.courses) payload = payload.data;
|
||||
if (!payload?.courses && payload?.version && payload?.courses == null) {
|
||||
throw new AppError('VALIDATION_FAILED', null, 'Invalid import payload');
|
||||
}
|
||||
|
||||
const result = await dataImportService.importData(payload);
|
||||
return successResponse(res, 200, 'Data imported successfully', result);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
// /components/dataImport/dataImportRoutes.js
|
||||
'use strict';
|
||||
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const dataImportController = require('./dataImportController');
|
||||
const authMiddleware = require('../../middlewares/authMiddleware');
|
||||
const requireSuperAdmin = require('../../middlewares/requireSuperAdmin');
|
||||
|
||||
const router = express.Router();
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 20 * 1024 * 1024 }
|
||||
});
|
||||
|
||||
router.use(authMiddleware);
|
||||
router.use(requireSuperAdmin);
|
||||
|
||||
router.post('/', upload.single('file'), dataImportController.importJson);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,258 @@
|
||||
// /components/dataImport/dataImportService.js
|
||||
'use strict';
|
||||
|
||||
const bcrypt = require('bcryptjs');
|
||||
const Course = require('../courses/courseModel');
|
||||
const Class = require('../classes/classModel');
|
||||
const User = require('../users/userModel');
|
||||
const Role = require('../roles/roleModel');
|
||||
const AppError = require('../../utils/AppError');
|
||||
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
|
||||
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
|
||||
|
||||
const toEnglishDigits = (value) =>
|
||||
String(value ?? '')
|
||||
.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d))
|
||||
.replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d));
|
||||
|
||||
const normalizePhone = (raw) => {
|
||||
if (raw == null || raw === '') return '';
|
||||
let digits = toEnglishDigits(raw).replace(/\D/g, '');
|
||||
if (digits.startsWith('98') && digits.length === 12) digits = `0${digits.slice(2)}`;
|
||||
if (digits.length === 10 && digits.startsWith('9')) digits = `0${digits}`;
|
||||
return digits;
|
||||
};
|
||||
|
||||
const normalizeNationalId = (raw) => {
|
||||
if (raw == null || raw === '') return '';
|
||||
return toEnglishDigits(raw).replace(/\D/g, '');
|
||||
};
|
||||
|
||||
const allocateUniqueUsername = async () => {
|
||||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||||
const username = generateUsername();
|
||||
if (!(await User.exists({ username }))) return username;
|
||||
}
|
||||
throw new AppError('INTERNAL_SERVER_ERROR', null, 'Could not generate a unique username');
|
||||
};
|
||||
|
||||
const allocatePlaceholderNationalId = async (phoneNumber) => {
|
||||
const base = `TMP${(phoneNumber || '').replace(/\D/g, '').slice(-10) || Date.now().toString().slice(-10)}`;
|
||||
let candidate = base.padEnd(10, '0').slice(0, 10);
|
||||
let i = 0;
|
||||
while (await User.exists({ nationalIdCode: candidate })) {
|
||||
i += 1;
|
||||
candidate = `${base.slice(0, 7)}${String(i).padStart(3, '0')}`.slice(0, 10);
|
||||
}
|
||||
return candidate;
|
||||
};
|
||||
|
||||
const findExistingUser = async ({ nationalIdCode, phoneNumber }) => {
|
||||
if (nationalIdCode) {
|
||||
const byId = await User.findOne({ nationalIdCode });
|
||||
if (byId) return byId;
|
||||
}
|
||||
if (phoneNumber) {
|
||||
const byPhone = await User.findOne({ phoneNumber });
|
||||
if (byPhone) return byPhone;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const applyStudentProfile = (user, student) => {
|
||||
const name = mergeFullName(student.name, student.surname);
|
||||
if (name) user.name = name;
|
||||
|
||||
const gender = normalizeGender(student.gender);
|
||||
if (gender) user.gender = gender;
|
||||
|
||||
const optionalFields = [
|
||||
'address',
|
||||
'birthCertificateNumber',
|
||||
'postalCode',
|
||||
'placeOfIssue',
|
||||
'fatherName',
|
||||
'education',
|
||||
'parentPhoneNumber'
|
||||
];
|
||||
for (const field of optionalFields) {
|
||||
if (student[field] && !user[field]) {
|
||||
user[field] = String(student[field]).trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (student.birthDate && !user.birthDate) {
|
||||
const date = new Date(student.birthDate);
|
||||
if (!Number.isNaN(date.getTime())) user.birthDate = date;
|
||||
}
|
||||
};
|
||||
|
||||
const upsertStudent = async (student, userRole, stats, warnings) => {
|
||||
const phoneNumber = normalizePhone(student.phoneNumber || student.phone || student.parentPhoneNumber);
|
||||
let nationalIdCode = normalizeNationalId(student.nationalIdCode || student.nationalId);
|
||||
|
||||
if (!phoneNumber && !nationalIdCode) {
|
||||
warnings.push({
|
||||
reason: 'missing_identity',
|
||||
student: { name: student.name }
|
||||
});
|
||||
stats.studentsSkipped += 1;
|
||||
return null;
|
||||
}
|
||||
|
||||
let user = await findExistingUser({ nationalIdCode, phoneNumber });
|
||||
if (user) {
|
||||
applyStudentProfile(user, student);
|
||||
if (phoneNumber && !user.phoneNumber) user.phoneNumber = phoneNumber;
|
||||
await user.save();
|
||||
stats.studentsUpdated += 1;
|
||||
return user;
|
||||
}
|
||||
|
||||
if (!phoneNumber) {
|
||||
warnings.push({
|
||||
reason: 'missing_phone',
|
||||
student: { name: student.name, nationalIdCode }
|
||||
});
|
||||
stats.studentsSkipped += 1;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!nationalIdCode) {
|
||||
nationalIdCode = await allocatePlaceholderNationalId(phoneNumber);
|
||||
warnings.push({
|
||||
reason: 'placeholder_national_id',
|
||||
student: { name: student.name, phoneNumber, nationalIdCode }
|
||||
});
|
||||
}
|
||||
|
||||
// Collision: national id exists with different phone, or reverse
|
||||
const conflictById = await User.findOne({ nationalIdCode });
|
||||
if (conflictById) {
|
||||
applyStudentProfile(conflictById, student);
|
||||
await conflictById.save();
|
||||
stats.studentsUpdated += 1;
|
||||
return conflictById;
|
||||
}
|
||||
|
||||
const username = await allocateUniqueUsername();
|
||||
const plainPassword = generateSimplePassword();
|
||||
const passwordHash = await bcrypt.hash(plainPassword, 10);
|
||||
|
||||
user = await User.create({
|
||||
name: mergeFullName(student.name, student.surname) || `کاربر ${phoneNumber}`,
|
||||
gender: normalizeGender(student.gender),
|
||||
nationalIdCode,
|
||||
phoneNumber,
|
||||
address: student.address,
|
||||
birthCertificateNumber: student.birthCertificateNumber,
|
||||
postalCode: student.postalCode,
|
||||
placeOfIssue: student.placeOfIssue,
|
||||
fatherName: student.fatherName,
|
||||
birthDate: student.birthDate ? new Date(student.birthDate) : undefined,
|
||||
education: student.education,
|
||||
parentPhoneNumber: student.parentPhoneNumber
|
||||
? normalizePhone(student.parentPhoneNumber)
|
||||
: undefined,
|
||||
username,
|
||||
passwordHash,
|
||||
role: userRole._id,
|
||||
preferredMessenger: ['SMS']
|
||||
});
|
||||
|
||||
stats.studentsCreated += 1;
|
||||
return user;
|
||||
};
|
||||
|
||||
const importData = async (payload) => {
|
||||
if (!payload || !Array.isArray(payload.courses)) {
|
||||
throw new AppError('VALIDATION_FAILED', null, 'Invalid import payload: courses array required');
|
||||
}
|
||||
|
||||
const userRole = await Role.findOne({ name: 'User' });
|
||||
if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
|
||||
|
||||
const stats = {
|
||||
coursesCreated: 0,
|
||||
coursesReused: 0,
|
||||
classesCreated: 0,
|
||||
classesReused: 0,
|
||||
studentsCreated: 0,
|
||||
studentsUpdated: 0,
|
||||
studentsSkipped: 0,
|
||||
enrollmentsAdded: 0
|
||||
};
|
||||
const warnings = [];
|
||||
|
||||
for (const courseInput of payload.courses) {
|
||||
const title = String(courseInput.title || '').trim();
|
||||
if (!title) continue;
|
||||
|
||||
const type = courseInput.type === 'Private' ? 'Private' : 'General';
|
||||
let course = await Course.findOne({ title });
|
||||
if (!course) {
|
||||
course = await Course.create({
|
||||
title,
|
||||
type,
|
||||
price: Number(courseInput.price) || 0,
|
||||
showOnFrontend: false,
|
||||
description: courseInput.description || ''
|
||||
});
|
||||
stats.coursesCreated += 1;
|
||||
} else {
|
||||
stats.coursesReused += 1;
|
||||
if (type === 'Private' && course.type !== 'Private') {
|
||||
course.type = 'Private';
|
||||
await course.save();
|
||||
}
|
||||
}
|
||||
|
||||
const classes = Array.isArray(courseInput.classes) ? courseInput.classes : [];
|
||||
for (const classInput of classes) {
|
||||
const className = String(classInput.name || '').trim();
|
||||
if (!className) continue;
|
||||
|
||||
let cls = await Class.findOne({ name: className, course: course._id });
|
||||
if (!cls) {
|
||||
cls = await Class.create({
|
||||
name: className,
|
||||
course: course._id,
|
||||
startDate: classInput.startDate ? new Date(classInput.startDate) : undefined,
|
||||
tuitionFee: Number(classInput.tuitionFee) || Number(courseInput.price) || 0,
|
||||
isActive: true
|
||||
});
|
||||
stats.classesCreated += 1;
|
||||
} else {
|
||||
stats.classesReused += 1;
|
||||
if (!cls.startDate && classInput.startDate) {
|
||||
cls.startDate = new Date(classInput.startDate);
|
||||
await cls.save();
|
||||
}
|
||||
}
|
||||
|
||||
const students = Array.isArray(classInput.students) ? classInput.students : [];
|
||||
for (const student of students) {
|
||||
const user = await upsertStudent(student, userRole, stats, warnings);
|
||||
if (!user) continue;
|
||||
|
||||
if (!user.courses.map(String).includes(String(course._id))) {
|
||||
user.courses.push(course._id);
|
||||
await user.save();
|
||||
}
|
||||
|
||||
if (!cls.students.map(String).includes(String(user._id))) {
|
||||
cls.students.push(user._id);
|
||||
stats.enrollmentsAdded += 1;
|
||||
}
|
||||
}
|
||||
|
||||
await cls.save();
|
||||
}
|
||||
}
|
||||
|
||||
return { stats, warnings };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
importData
|
||||
};
|
||||
@@ -37,7 +37,7 @@ const createNotification = async (data) => {
|
||||
};
|
||||
|
||||
const getNotificationById = async (id) => {
|
||||
const notification = await Notification.findById(id).populate('user', 'name surname username email phoneNumber');
|
||||
const notification = await Notification.findById(id).populate('user', 'name username email phoneNumber');
|
||||
if (!notification) {
|
||||
throw new AppError('NOTIFICATION_NOT_FOUND');
|
||||
}
|
||||
@@ -49,7 +49,7 @@ const getAllNotifications = async (queryParams) => {
|
||||
const filter = buildFilterQuery(queryParams, ['subject', 'body']);
|
||||
|
||||
const [notifications, totalCount] = await Promise.all([
|
||||
Notification.find(filter).populate('user', 'name surname username').sort(sort).skip(skip).limit(limit),
|
||||
Notification.find(filter).populate('user', 'name username').sort(sort).skip(skip).limit(limit),
|
||||
Notification.countDocuments(filter)
|
||||
]);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const getAllPayments = async (query) => {
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
Payment.find(filter)
|
||||
.populate({ path: 'user', select: 'name surname' })
|
||||
.populate({ path: 'user', select: 'name' })
|
||||
.populate({ path: 'classes', select: 'name' })
|
||||
.populate({ path: 'course', select: 'title' })
|
||||
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
|
||||
@@ -30,7 +30,7 @@ const getAllPayments = async (query) => {
|
||||
|
||||
const getPaymentById = async (id) => {
|
||||
const payment = await Payment.findById(id)
|
||||
.populate({ path: 'user', select: 'name surname phoneNumber' })
|
||||
.populate({ path: 'user', select: 'name phoneNumber' })
|
||||
.populate({ path: 'classes', select: 'name tuitionFee' })
|
||||
.populate({ path: 'course', select: 'title price' })
|
||||
.lean();
|
||||
|
||||
@@ -85,10 +85,10 @@ const getSessionById = async (id) => {
|
||||
.populate({
|
||||
path: 'class',
|
||||
select: 'name students capacity',
|
||||
populate: { path: 'students', select: 'name surname nationalIdCode phoneNumber' }
|
||||
populate: { path: 'students', select: 'name nationalIdCode phoneNumber gender' }
|
||||
})
|
||||
.populate('professor', 'name surname email phoneNumber')
|
||||
.populate('attendanceList.user', 'name surname username nationalIdCode');
|
||||
.populate('attendanceList.user', 'name username nationalIdCode');
|
||||
if (!session) {
|
||||
throw new AppError('SESSION_NOT_FOUND');
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ const userSchema = new mongoose.Schema({
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
surname: {
|
||||
gender: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['male', 'female'],
|
||||
trim: true
|
||||
},
|
||||
phoneNumber: {
|
||||
@@ -51,6 +51,33 @@ const userSchema = new mongoose.Schema({
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
birthCertificateNumber: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
postalCode: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
placeOfIssue: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
fatherName: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
birthDate: {
|
||||
type: Date
|
||||
},
|
||||
education: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
parentPhoneNumber: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
@@ -9,6 +9,7 @@ const { calculateMeta } = require('../../utils/pagination');
|
||||
const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
|
||||
const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages');
|
||||
const logger = require('../../utils/logger');
|
||||
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
|
||||
|
||||
const POPULATE_ROLE = { path: 'role', select: 'name permissions' };
|
||||
const SAFE_FIELDS = '-passwordHash -refreshTokens';
|
||||
@@ -37,23 +38,46 @@ const allocateUniqueUsername = async (preferred) => {
|
||||
throw new AppError('INTERNAL_SERVER_ERROR', null, 'Could not generate a unique username');
|
||||
};
|
||||
|
||||
const pickProfileFields = (body = {}) => {
|
||||
const fullName = mergeFullName(body.name, body.surname);
|
||||
const fields = {
|
||||
name: fullName || undefined,
|
||||
gender: normalizeGender(body.gender),
|
||||
nationalIdCode: body.nationalIdCode || body.nationalId || undefined,
|
||||
phoneNumber: body.phoneNumber || body.phone || undefined,
|
||||
email: body.email,
|
||||
address: body.address,
|
||||
birthCertificateNumber: body.birthCertificateNumber,
|
||||
postalCode: body.postalCode,
|
||||
placeOfIssue: body.placeOfIssue,
|
||||
fatherName: body.fatherName,
|
||||
birthDate: body.birthDate || undefined,
|
||||
education: body.education,
|
||||
parentPhoneNumber: body.parentPhoneNumber,
|
||||
preferredMessenger: normalizePreferredMessengers(body.preferredMessenger)
|
||||
};
|
||||
|
||||
Object.keys(fields).forEach((key) => {
|
||||
if (fields[key] === '' || fields[key] === null || fields[key] === undefined) {
|
||||
delete fields[key];
|
||||
}
|
||||
});
|
||||
|
||||
return fields;
|
||||
};
|
||||
|
||||
const signUp = async (body) => {
|
||||
const { name, surname, nationalId, nationalIdCode, phoneNumber, phone, username, password, email, address, preferredMessenger } = body;
|
||||
|
||||
const profile = pickProfileFields(body);
|
||||
const { username, password } = body;
|
||||
|
||||
const userRole = await Role.findOne({ name: 'User' });
|
||||
if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
const user = await User.create({
|
||||
name,
|
||||
surname,
|
||||
nationalIdCode: nationalIdCode || nationalId,
|
||||
phoneNumber: phoneNumber || phone,
|
||||
...profile,
|
||||
username,
|
||||
passwordHash,
|
||||
email,
|
||||
address,
|
||||
preferredMessenger: normalizePreferredMessengers(preferredMessenger),
|
||||
role: userRole._id
|
||||
});
|
||||
|
||||
@@ -75,8 +99,9 @@ const getAllUsers = async (query) => {
|
||||
if (query.search) {
|
||||
filter.$or = [
|
||||
{ name: new RegExp(query.search, 'i') },
|
||||
{ surname: new RegExp(query.search, 'i') },
|
||||
{ username: new RegExp(query.search, 'i') }
|
||||
{ username: new RegExp(query.search, 'i') },
|
||||
{ nationalIdCode: new RegExp(query.search, 'i') },
|
||||
{ phoneNumber: new RegExp(query.search, 'i') }
|
||||
];
|
||||
}
|
||||
if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
|
||||
@@ -92,20 +117,12 @@ const getAllUsers = async (query) => {
|
||||
const searchUsers = async (query) => getAllUsers(query);
|
||||
|
||||
const createUserAdmin = async (body) => {
|
||||
const profile = pickProfileFields(body);
|
||||
const {
|
||||
name,
|
||||
surname,
|
||||
nationalIdCode,
|
||||
nationalId,
|
||||
phoneNumber,
|
||||
phone,
|
||||
username: requestedUsername,
|
||||
password: requestedPassword,
|
||||
email,
|
||||
roleId,
|
||||
role,
|
||||
address,
|
||||
preferredMessenger,
|
||||
role
|
||||
} = body;
|
||||
|
||||
let roleObj = null;
|
||||
@@ -120,25 +137,18 @@ const createUserAdmin = async (body) => {
|
||||
const plainPassword = (requestedPassword && String(requestedPassword).trim()) || generateSimplePassword();
|
||||
const username = await allocateUniqueUsername(requestedUsername);
|
||||
const passwordHash = await bcrypt.hash(plainPassword, 10);
|
||||
const resolvedPhone = phoneNumber || phone;
|
||||
|
||||
const user = await User.create({
|
||||
name,
|
||||
surname,
|
||||
nationalIdCode: nationalIdCode || nationalId,
|
||||
phoneNumber: resolvedPhone,
|
||||
...profile,
|
||||
username,
|
||||
passwordHash,
|
||||
email,
|
||||
address,
|
||||
preferredMessenger: normalizePreferredMessengers(preferredMessenger),
|
||||
role: roleObj._id,
|
||||
role: roleObj._id
|
||||
});
|
||||
|
||||
try {
|
||||
await sendAccountCreatedSms(resolvedPhone, username, plainPassword);
|
||||
await sendAccountCreatedSms(profile.phoneNumber, username, plainPassword);
|
||||
} catch (err) {
|
||||
logger.error(`[createUserAdmin] Account SMS failed for ${resolvedPhone}: ${err.message}`);
|
||||
logger.error(`[createUserAdmin] Account SMS failed for ${profile.phoneNumber}: ${err.message}`);
|
||||
}
|
||||
|
||||
const created = await User.findById(user._id).select(SAFE_FIELDS).populate(POPULATE_ROLE).lean();
|
||||
@@ -146,8 +156,8 @@ const createUserAdmin = async (body) => {
|
||||
...created,
|
||||
generatedCredentials: {
|
||||
username,
|
||||
password: plainPassword,
|
||||
},
|
||||
password: plainPassword
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -164,6 +174,9 @@ const updateUser = async (id, body) => {
|
||||
passwordHash,
|
||||
refreshTokens,
|
||||
preferredMessenger,
|
||||
surname,
|
||||
name,
|
||||
gender,
|
||||
_id,
|
||||
id: bodyId,
|
||||
createdAt,
|
||||
@@ -173,8 +186,12 @@ const updateUser = async (id, body) => {
|
||||
} = body;
|
||||
|
||||
const update = { ...rest };
|
||||
const mergedName = mergeFullName(name, surname);
|
||||
if (mergedName) update.name = mergedName;
|
||||
|
||||
const normalizedGender = normalizeGender(gender);
|
||||
if (normalizedGender) update.gender = normalizedGender;
|
||||
|
||||
// Map frontend field aliases to schema fields
|
||||
if (nationalIdCode || nationalId) {
|
||||
update.nationalIdCode = nationalIdCode || nationalId;
|
||||
}
|
||||
@@ -188,12 +205,13 @@ const updateUser = async (id, body) => {
|
||||
update.preferredMessenger = normalizePreferredMessengers(preferredMessenger) || [];
|
||||
}
|
||||
|
||||
// Never overwrite username with an empty value on update
|
||||
if (typeof username === 'string' && username.trim()) {
|
||||
update.username = username.trim();
|
||||
}
|
||||
|
||||
// Strip empty strings so required validators are not tripped
|
||||
// Legacy field — drop if clients still send it
|
||||
delete update.surname;
|
||||
|
||||
Object.keys(update).forEach((key) => {
|
||||
if (key === 'preferredMessenger') return;
|
||||
if (update[key] === '' || update[key] === null || update[key] === undefined) {
|
||||
@@ -218,7 +236,7 @@ const deleteUser = async (id) => {
|
||||
if (!user) throw new AppError('USER_NOT_FOUND');
|
||||
};
|
||||
|
||||
const enrollUserInCourse = async (userId, courseId, actorId) => {
|
||||
const enrollUserInCourse = async (userId, courseId) => {
|
||||
const user = await User.findById(userId);
|
||||
if (!user) throw new AppError('USER_NOT_FOUND');
|
||||
|
||||
@@ -229,4 +247,13 @@ const enrollUserInCourse = async (userId, courseId, actorId) => {
|
||||
return User.findById(userId).select(SAFE_FIELDS).populate('courses').lean();
|
||||
};
|
||||
|
||||
module.exports = { signUp, getUserById, getAllUsers, searchUsers, createUserAdmin, updateUser, deleteUser, enrollUserInCourse };
|
||||
module.exports = {
|
||||
signUp,
|
||||
getUserById,
|
||||
getAllUsers,
|
||||
searchUsers,
|
||||
createUserAdmin,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
enrollUserInCourse
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -76,7 +76,7 @@ const activityLogger = (req, res, next) => {
|
||||
actor: actor?._id || null,
|
||||
actorUsername: actor?.username || req.body?.username || null,
|
||||
actorName: actor
|
||||
? `${actor.name || ''} ${actor.surname || ''}`.trim() || actor.username
|
||||
? `${actor.name || ''}`.trim() || actor.username
|
||||
: req.body?.username || null,
|
||||
action,
|
||||
resource: resolveResource(path),
|
||||
|
||||
Generated
+100
-514
@@ -12,7 +12,6 @@
|
||||
"@aws-sdk/client-s3": "^3.1106.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1106.0",
|
||||
"axios": "^1.7.9",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.7",
|
||||
@@ -25,7 +24,8 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^6.10.0",
|
||||
"winston": "^3.17.0"
|
||||
"winston": "^3.17.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.9"
|
||||
@@ -391,26 +391,6 @@
|
||||
"@hapi/hoek": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mapbox/node-pre-gyp": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
|
||||
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"make-dir": "^3.1.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"nopt": "^5.0.0",
|
||||
"npmlog": "^5.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"semver": "^7.3.5",
|
||||
"tar": "^6.1.11"
|
||||
},
|
||||
"bin": {
|
||||
"node-pre-gyp": "bin/node-pre-gyp"
|
||||
}
|
||||
},
|
||||
"node_modules/@mongodb-js/saslprep": {
|
||||
"version": "1.4.13",
|
||||
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.13.tgz",
|
||||
@@ -553,12 +533,6 @@
|
||||
"@types/webidl-conversions": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
@@ -572,6 +546,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/adler-32": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
|
||||
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@@ -584,15 +567,6 @@
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
@@ -613,40 +587,6 @@
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||
"integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/are-we-there-yet": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
|
||||
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/are-we-there-yet/node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
@@ -687,20 +627,6 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
|
||||
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mapbox/node-pre-gyp": "^1.0.11",
|
||||
"node-addon-api": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
@@ -864,6 +790,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/cfb": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
|
||||
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"crc-32": "~1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@@ -889,13 +828,13 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
|
||||
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
|
||||
"license": "ISC",
|
||||
"node_modules/codepage": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
|
||||
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/color": {
|
||||
@@ -944,15 +883,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -965,12 +895,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
|
||||
@@ -986,12 +910,6 @@
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -1051,6 +969,18 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/crc-32": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -1077,12 +1007,6 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -1102,15 +1026,6 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
@@ -1152,12 +1067,6 @@
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
|
||||
@@ -1412,6 +1321,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/frac": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
|
||||
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
@@ -1421,36 +1339,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
|
||||
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-minipass/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@@ -1475,27 +1363,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gauge": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
|
||||
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"aproba": "^1.0.3 || ^2.0.0",
|
||||
"color-support": "^1.1.2",
|
||||
"console-control-strings": "^1.0.0",
|
||||
"has-unicode": "^2.0.1",
|
||||
"object-assign": "^4.1.1",
|
||||
"signal-exit": "^3.0.0",
|
||||
"string-width": "^4.2.3",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wide-align": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
@@ -1533,27 +1400,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.1.1",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
@@ -1567,34 +1413,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -1644,12 +1462,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
@@ -1726,17 +1538,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
@@ -1775,15 +1576,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
@@ -1955,30 +1747,6 @@
|
||||
"node": ">= 12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -2079,40 +1847,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
|
||||
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
|
||||
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^3.0.0",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib/node_modules/minipass": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
|
||||
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
@@ -2258,12 +1992,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
|
||||
@@ -2276,48 +2004,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/node-fetch/node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.10.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
|
||||
@@ -2356,21 +2042,6 @@
|
||||
"url": "https://opencollective.com/nodemon"
|
||||
}
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"abbrev": "1"
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -2381,19 +2052,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/npmlog": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
|
||||
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
|
||||
"deprecated": "This package is no longer supported.",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"are-we-there-yet": "^2.0.0",
|
||||
"console-control-strings": "^1.1.0",
|
||||
"gauge": "^3.0.0",
|
||||
"set-blocking": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -2427,15 +2085,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/one-time": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
|
||||
@@ -2454,15 +2103,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
@@ -2600,22 +2240,6 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rimraf": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"glob": "^7.1.3"
|
||||
},
|
||||
"bin": {
|
||||
"rimraf": "bin.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -2717,12 +2341,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -2807,12 +2425,6 @@
|
||||
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||
@@ -2835,6 +2447,18 @@
|
||||
"memory-pager": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ssf": {
|
||||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
|
||||
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"frac": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-trace": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
|
||||
@@ -2876,32 +2500,6 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
@@ -2915,36 +2513,6 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
"minipass": "^5.0.0",
|
||||
"minizlib": "^2.1.1",
|
||||
"mkdirp": "^1.0.3",
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/tar/node_modules/mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/text-hex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
@@ -3101,15 +2669,6 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/wide-align": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
|
||||
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^1.0.2 || 2 || 3 || 4"
|
||||
}
|
||||
},
|
||||
"node_modules/winston": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
|
||||
@@ -3174,11 +2733,44 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"node_modules/wmf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
|
||||
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/word": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
|
||||
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xlsx": {
|
||||
"version": "0.18.5",
|
||||
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
|
||||
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"adler-32": "~1.3.0",
|
||||
"cfb": "~1.2.1",
|
||||
"codepage": "~1.15.0",
|
||||
"crc-32": "~1.2.1",
|
||||
"ssf": "~0.11.2",
|
||||
"wmf": "~1.0.1",
|
||||
"word": "~0.3.0"
|
||||
},
|
||||
"bin": {
|
||||
"xlsx": "bin/xlsx.njs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
@@ -3188,12 +2780,6 @@
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"node-cron": "^3.0.3",
|
||||
"nodemailer": "^6.10.0",
|
||||
"winston": "^3.17.0"
|
||||
"winston": "^3.17.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.9"
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Converts class-folder xlsx files under raw-data into a structured import JSON.
|
||||
* Student photo folders are ignored.
|
||||
*
|
||||
* Usage: node scripts/convert-raw-data.js [raw-data-root] [output.json]
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const XLSX = require('xlsx');
|
||||
|
||||
const MONTHS = {
|
||||
فروردین: 1,
|
||||
اردیبهشت: 2,
|
||||
خرداد: 3,
|
||||
تیر: 4,
|
||||
مرداد: 5,
|
||||
شهریور: 6,
|
||||
مهر: 7,
|
||||
آبان: 8,
|
||||
آذر: 9,
|
||||
دی: 10,
|
||||
بهمن: 11,
|
||||
اسفند: 12
|
||||
};
|
||||
|
||||
const INFO_SHEET_HINTS = ['اطلاعات کلی', 'مشخصات کلی', 'اطلاعات'];
|
||||
const FALLBACK_SHEET_HINTS = ['شهریه'];
|
||||
|
||||
const DEFAULT_RAW = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'raw-data',
|
||||
'برنامه آموزشی 1405-20260814T213206Z-1-001',
|
||||
'برنامه آموزشی 1405'
|
||||
);
|
||||
const DEFAULT_OUT = path.join(__dirname, '..', 'data', 'import-data.json');
|
||||
|
||||
const toEnglishDigits = (value) =>
|
||||
String(value ?? '')
|
||||
.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d))
|
||||
.replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d));
|
||||
|
||||
const cleanText = (value) => {
|
||||
if (value == null) return '';
|
||||
return String(value).replace(/\s+/g, ' ').trim();
|
||||
};
|
||||
|
||||
const normalizeHeader = (value) =>
|
||||
cleanText(value)
|
||||
.replace(/\n/g, ' ')
|
||||
.replace(/\s+/g, ' ');
|
||||
|
||||
const jalaliToGregorian = (jy, jm, jd) => {
|
||||
const gy = jy <= 979 ? 621 : 1600;
|
||||
jy -= jy <= 979 ? 0 : 979;
|
||||
let days =
|
||||
365 * jy +
|
||||
Math.floor(jy / 33) * 8 +
|
||||
Math.floor(((jy % 33) + 3) / 4) +
|
||||
78 +
|
||||
jd +
|
||||
(jm < 7 ? (jm - 1) * 31 : (jm - 7) * 30 + 186);
|
||||
let gyOut = gy + 400 * Math.floor(days / 146097);
|
||||
days %= 146097;
|
||||
if (days > 36524) {
|
||||
gyOut += 100 * Math.floor(--days / 36524);
|
||||
days %= 36524;
|
||||
if (days >= 365) days += 1;
|
||||
}
|
||||
gyOut += 4 * Math.floor(days / 1461);
|
||||
days %= 1461;
|
||||
if (days > 365) {
|
||||
gyOut += Math.floor((days - 1) / 365);
|
||||
days = (days - 1) % 365;
|
||||
}
|
||||
let gd = days + 1;
|
||||
const sal_a = [
|
||||
0,
|
||||
31,
|
||||
(gyOut % 4 === 0 && gyOut % 100 !== 0) || gyOut % 400 === 0 ? 29 : 28,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31
|
||||
];
|
||||
let gm = 0;
|
||||
for (gm = 1; gm <= 12 && gd > sal_a[gm]; gm += 1) gd -= sal_a[gm];
|
||||
const mm = String(gm).padStart(2, '0');
|
||||
const dd = String(gd).padStart(2, '0');
|
||||
return `${gyOut}-${mm}-${dd}`;
|
||||
};
|
||||
|
||||
const parseJalaliDate = (raw) => {
|
||||
if (!raw) return null;
|
||||
const text = toEnglishDigits(raw).replace(/[./\-]/g, '/').trim();
|
||||
const match = text.match(/^(\d{3,4})\/(\d{1,2})\/(\d{1,2})$/);
|
||||
if (!match) return null;
|
||||
const jy = Number(match[1]);
|
||||
const jm = Number(match[2]);
|
||||
const jd = Number(match[3]);
|
||||
if (!jy || !jm || !jd || jm > 12 || jd > 31) return null;
|
||||
try {
|
||||
return jalaliToGregorian(jy, jm, jd);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizePhone = (raw) => {
|
||||
if (raw == null || raw === '') return '';
|
||||
let digits = toEnglishDigits(raw).replace(/[^\d+]/g, '');
|
||||
if (digits.includes('-') || String(raw).includes('-')) {
|
||||
// Prefer first Iranian mobile-looking segment
|
||||
const parts = toEnglishDigits(raw)
|
||||
.split(/[-–—,\/|\s]+/)
|
||||
.map((p) => p.replace(/\D/g, ''))
|
||||
.filter(Boolean);
|
||||
const candidate =
|
||||
parts.find((p) => /^(0?9\d{9})$/.test(p)) ||
|
||||
parts.find((p) => p.length >= 10) ||
|
||||
parts[0] ||
|
||||
'';
|
||||
digits = candidate;
|
||||
}
|
||||
digits = digits.replace(/\D/g, '');
|
||||
if (digits.startsWith('98') && digits.length === 12) digits = `0${digits.slice(2)}`;
|
||||
if (digits.length === 10 && digits.startsWith('9')) digits = `0${digits}`;
|
||||
if (digits.length === 11 && digits.startsWith('09')) return digits;
|
||||
return digits || '';
|
||||
};
|
||||
|
||||
const normalizeNationalId = (raw) => {
|
||||
if (raw == null || raw === '') return '';
|
||||
const digits = toEnglishDigits(raw).replace(/\D/g, '');
|
||||
return digits;
|
||||
};
|
||||
|
||||
const parseFolderMeta = (folderName) => {
|
||||
const cleaned = cleanText(folderName).replace(/^\d+\s*[-–—.]?\s*/, '');
|
||||
const monthNames = Object.keys(MONTHS).join('|');
|
||||
const monthMatch = cleaned.match(new RegExp(`(${monthNames})\\s+(\\d{4})$`));
|
||||
let className = cleaned;
|
||||
let courseTitle = cleaned;
|
||||
let jalaliYear = null;
|
||||
let jalaliMonth = null;
|
||||
let startDate = null;
|
||||
|
||||
if (monthMatch) {
|
||||
const monthName = monthMatch[1];
|
||||
jalaliYear = Number(monthMatch[2]);
|
||||
jalaliMonth = MONTHS[monthName];
|
||||
className = cleaned;
|
||||
courseTitle = cleanText(cleaned.slice(0, monthMatch.index));
|
||||
startDate = jalaliToGregorian(jalaliYear, jalaliMonth, 1);
|
||||
}
|
||||
|
||||
const isPrivate = /خصوصی/.test(courseTitle) || /خصوصی/.test(className);
|
||||
if (isPrivate) {
|
||||
courseTitle = cleanText(courseTitle.replace(/خصوصی/g, ''));
|
||||
}
|
||||
|
||||
return {
|
||||
folderName,
|
||||
courseTitle: courseTitle || className,
|
||||
className,
|
||||
type: isPrivate ? 'Private' : 'General',
|
||||
jalaliYear,
|
||||
jalaliMonth,
|
||||
startDate
|
||||
};
|
||||
};
|
||||
|
||||
const findHeaderMap = (headerRow) => {
|
||||
const map = {};
|
||||
(headerRow || []).forEach((cell, idx) => {
|
||||
const h = normalizeHeader(cell);
|
||||
if (!h) return;
|
||||
if (h === 'نام' || h === 'نام ') map.firstName = idx;
|
||||
else if (h.includes('نام خانوادگی')) map.lastName = idx;
|
||||
else if (h.includes('نام و نام خانوادگی')) map.fullName = idx;
|
||||
else if (h.includes('تلفن همراه هنرجو') || h.includes('شماره تماس هنرجو')) map.phone = idx;
|
||||
else if (h.includes('تلفن همراه والد')) map.parentPhone = idx;
|
||||
else if (h.includes('تلفن همراه') || h.includes('شماره تماس') || h.includes('شماره همراه')) {
|
||||
if (map.phone == null) map.phone = idx;
|
||||
} else if (h.includes('کد ملی')) map.nationalId = idx;
|
||||
else if (h.includes('شماره شناسنامه')) map.birthCertificateNumber = idx;
|
||||
else if (h.includes('کد پستی')) map.postalCode = idx;
|
||||
else if (h.includes('محل صدور')) map.placeOfIssue = idx;
|
||||
else if (h.includes('نام پدر')) map.fatherName = idx;
|
||||
else if (h.includes('تاریخ تولد')) map.birthDate = idx;
|
||||
else if (h.includes('تحصیلات')) map.education = idx;
|
||||
else if (h === 'آدرس' || h.includes('آدرس')) map.address = idx;
|
||||
});
|
||||
return map;
|
||||
};
|
||||
|
||||
const pickSheet = (wb, hints) => {
|
||||
for (const hint of hints) {
|
||||
const found = wb.SheetNames.find((n) => normalizeHeader(n) === hint || normalizeHeader(n).includes(hint));
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const rowToStudent = (row, map) => {
|
||||
const get = (key) => (map[key] == null ? '' : cleanText(row[map[key]]));
|
||||
|
||||
let name = '';
|
||||
if (map.fullName != null) {
|
||||
name = get('fullName');
|
||||
} else {
|
||||
name = cleanText(`${get('firstName')} ${get('lastName')}`);
|
||||
}
|
||||
|
||||
const phone = normalizePhone(get('phone') || get('parentPhone'));
|
||||
const parentPhone = map.parentPhone != null ? normalizePhone(get('parentPhone')) : '';
|
||||
const nationalIdCode = normalizeNationalId(get('nationalId'));
|
||||
const birthDateRaw = get('birthDate');
|
||||
const birthDate = parseJalaliDate(birthDateRaw);
|
||||
|
||||
if (!name && !phone && !nationalIdCode) return null;
|
||||
|
||||
const student = {
|
||||
name: name || (nationalIdCode ? `کاربر ${nationalIdCode}` : `کاربر ${phone}`),
|
||||
phoneNumber: phone || undefined,
|
||||
nationalIdCode: nationalIdCode || undefined,
|
||||
birthCertificateNumber: get('birthCertificateNumber') || undefined,
|
||||
postalCode: get('postalCode') || undefined,
|
||||
placeOfIssue: get('placeOfIssue') || undefined,
|
||||
fatherName: get('fatherName') || undefined,
|
||||
birthDate: birthDate || undefined,
|
||||
birthDateJalali: birthDateRaw || undefined,
|
||||
education: get('education') || undefined,
|
||||
address: get('address') || undefined
|
||||
};
|
||||
|
||||
if (parentPhone && parentPhone !== phone) {
|
||||
student.parentPhoneNumber = parentPhone;
|
||||
}
|
||||
|
||||
// Drop undefined keys for cleaner JSON
|
||||
Object.keys(student).forEach((k) => {
|
||||
if (student[k] === undefined || student[k] === '') delete student[k];
|
||||
});
|
||||
|
||||
return student;
|
||||
};
|
||||
|
||||
const extractStudentsFromSheet = (sheet) => {
|
||||
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: null, raw: false });
|
||||
let headerIdx = -1;
|
||||
let map = {};
|
||||
|
||||
for (let i = 0; i < Math.min(rows.length, 15); i += 1) {
|
||||
const candidate = findHeaderMap(rows[i]);
|
||||
const hasIdentity =
|
||||
candidate.firstName != null ||
|
||||
candidate.fullName != null ||
|
||||
candidate.lastName != null;
|
||||
const hasContact = candidate.phone != null || candidate.nationalId != null;
|
||||
if (hasIdentity && (hasContact || candidate.lastName != null)) {
|
||||
headerIdx = i;
|
||||
map = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (headerIdx < 0) return [];
|
||||
|
||||
const students = [];
|
||||
for (let i = headerIdx + 1; i < rows.length; i += 1) {
|
||||
const row = rows[i];
|
||||
if (!row || row.every((c) => c == null || String(c).trim() === '')) continue;
|
||||
const student = rowToStudent(row, map);
|
||||
if (student) students.push(student);
|
||||
}
|
||||
return students;
|
||||
};
|
||||
|
||||
const findClassXlsx = (dirPath) => {
|
||||
const files = fs.readdirSync(dirPath).filter((f) => f.endsWith('.xlsx') && !f.startsWith('~$'));
|
||||
if (files.length === 0) return null;
|
||||
// Prefer file whose name looks like the class (not "لیست شرکت کنندگان...")
|
||||
const preferred = files.find((f) => !f.includes('لیست شرکت کنندگان')) || files[0];
|
||||
return path.join(dirPath, preferred);
|
||||
};
|
||||
|
||||
const convertClassFolder = (dirPath, folderName) => {
|
||||
const meta = parseFolderMeta(folderName);
|
||||
const xlsxPath = findClassXlsx(dirPath);
|
||||
let students = [];
|
||||
let sourceFile = null;
|
||||
|
||||
if (xlsxPath) {
|
||||
sourceFile = path.basename(xlsxPath);
|
||||
const wb = XLSX.readFile(xlsxPath, { cellDates: false, raw: false });
|
||||
const infoSheetName = pickSheet(wb, INFO_SHEET_HINTS);
|
||||
if (infoSheetName) {
|
||||
students = extractStudentsFromSheet(wb.Sheets[infoSheetName]);
|
||||
}
|
||||
if (students.length === 0) {
|
||||
const fallback = pickSheet(wb, FALLBACK_SHEET_HINTS);
|
||||
if (fallback) students = extractStudentsFromSheet(wb.Sheets[fallback]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...meta,
|
||||
sourceFile,
|
||||
students
|
||||
};
|
||||
};
|
||||
|
||||
const buildImportDocument = (rawRoot) => {
|
||||
const entries = fs
|
||||
.readdirSync(rawRoot, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name)
|
||||
.sort((a, b) => a.localeCompare(b, 'fa'));
|
||||
|
||||
const classes = entries.map((name) => convertClassFolder(path.join(rawRoot, name), name));
|
||||
|
||||
const coursesMap = new Map();
|
||||
for (const cls of classes) {
|
||||
const key = cls.courseTitle;
|
||||
if (!coursesMap.has(key)) {
|
||||
coursesMap.set(key, {
|
||||
title: cls.courseTitle,
|
||||
type: cls.type,
|
||||
price: 0,
|
||||
classes: []
|
||||
});
|
||||
}
|
||||
const course = coursesMap.get(key);
|
||||
if (cls.type === 'Private') course.type = 'Private';
|
||||
course.classes.push({
|
||||
name: cls.className,
|
||||
folderName: cls.folderName,
|
||||
startDate: cls.startDate,
|
||||
jalaliYear: cls.jalaliYear,
|
||||
jalaliMonth: cls.jalaliMonth,
|
||||
sourceFile: cls.sourceFile,
|
||||
students: cls.students
|
||||
});
|
||||
}
|
||||
|
||||
const courses = [...coursesMap.values()];
|
||||
const userCount = classes.reduce((n, c) => n + c.students.length, 0);
|
||||
|
||||
return {
|
||||
version: 1,
|
||||
generatedAt: new Date().toISOString(),
|
||||
source: path.basename(rawRoot),
|
||||
summary: {
|
||||
courses: courses.length,
|
||||
classes: classes.length,
|
||||
students: userCount
|
||||
},
|
||||
courses
|
||||
};
|
||||
};
|
||||
|
||||
const main = () => {
|
||||
const rawRoot = path.resolve(process.argv[2] || DEFAULT_RAW);
|
||||
const outPath = path.resolve(process.argv[3] || DEFAULT_OUT);
|
||||
|
||||
if (!fs.existsSync(rawRoot)) {
|
||||
console.error(`Raw data folder not found: ${rawRoot}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const doc = buildImportDocument(rawRoot);
|
||||
fs.writeFileSync(outPath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
|
||||
console.log(`Wrote ${outPath}`);
|
||||
console.log(JSON.stringify(doc.summary, null, 2));
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -146,8 +146,7 @@ const seedDatabase = async ({ disconnectOnComplete = false } = {}) => {
|
||||
if (!adminUser) {
|
||||
const passwordHash = await bcrypt.hash(config.SUPERADMIN_PASSWORD, 10);
|
||||
await User.create({
|
||||
name: 'مدیر',
|
||||
surname: 'ارشد',
|
||||
name: 'مدیر ارشد',
|
||||
nationalIdCode: config.SUPERADMIN_NATIONAL_ID,
|
||||
phoneNumber: config.SUPERADMIN_PHONE,
|
||||
email: config.SUPERADMIN_EMAIL || undefined,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// /utils/userProfile.js
|
||||
'use strict';
|
||||
|
||||
const ALLOWED_GENDERS = ['male', 'female'];
|
||||
|
||||
const mergeFullName = (name, surname) => {
|
||||
const parts = [name, surname].map((v) => (v == null ? '' : String(v).trim())).filter(Boolean);
|
||||
return parts.join(' ').trim();
|
||||
};
|
||||
|
||||
const normalizeGender = (value) => {
|
||||
if (value == null || value === '') return undefined;
|
||||
const g = String(value).trim().toLowerCase();
|
||||
if (ALLOWED_GENDERS.includes(g)) return g;
|
||||
if (g === 'مرد' || g === 'آقا') return 'male';
|
||||
if (g === 'زن' || g === 'خانم') return 'female';
|
||||
return undefined;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
ALLOWED_GENDERS,
|
||||
mergeFullName,
|
||||
normalizeGender
|
||||
};
|
||||
Reference in New Issue
Block a user