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:
2026-08-15 01:31:44 +03:30
parent 35704409ec
commit bf5e66fa95
19 changed files with 2314 additions and 569 deletions
+2
View File
@@ -79,6 +79,7 @@ app.get('/api/health', (req, res) => {
const seedDatabase = require('./seed'); const seedDatabase = require('./seed');
const seedRoutes = require('./components/seed/seedRoutes'); const seedRoutes = require('./components/seed/seedRoutes');
const dataImportRoutes = require('./components/dataImport/dataImportRoutes');
app.use('/api/auth', authRoutes); app.use('/api/auth', authRoutes);
app.use('/api/users', userRoutes); app.use('/api/users', userRoutes);
@@ -95,6 +96,7 @@ app.use('/api/dashboard', dashboardRoutes);
app.use('/api/activity-logs', activityLogRoutes); app.use('/api/activity-logs', activityLogRoutes);
app.use('/api/contact-inquiries', contactInquiryRoutes); app.use('/api/contact-inquiries', contactInquiryRoutes);
app.use('/api/seed', seedRoutes); app.use('/api/seed', seedRoutes);
app.use('/api/data-import', dataImportRoutes);
// ── Error Handlers ──────────────────────────────────────────────────────────── // ── Error Handlers ────────────────────────────────────────────────────────────
app.use(notFoundHandler); app.use(notFoundHandler);
@@ -121,7 +121,7 @@ const getAllActivityLogs = async (queryParams = {}) => {
const [logs, totalCount] = await Promise.all([ const [logs, totalCount] = await Promise.all([
ActivityLog.find(filter) ActivityLog.find(filter)
.populate('actor', 'name surname username') .populate('actor', 'name username')
.sort(sort) .sort(sort)
.skip(skip) .skip(skip)
.limit(limit) .limit(limit)
@@ -39,7 +39,7 @@ const createCertificate = async (data) => {
const getCertificateById = async (id) => { const getCertificateById = async (id) => {
const certificate = await Certificate.findById(id) const certificate = await Certificate.findById(id)
.populate('user', 'name surname username nationalIdCode') .populate('user', 'name username nationalIdCode')
.populate('course', 'title type'); .populate('course', 'title type');
if (!certificate) { if (!certificate) {
throw new AppError('CERTIFICATE_NOT_FOUND'); throw new AppError('CERTIFICATE_NOT_FOUND');
@@ -56,7 +56,7 @@ const getAllCertificates = async (queryParams) => {
const [certificates, totalCount] = await Promise.all([ const [certificates, totalCount] = await Promise.all([
Certificate.find(filter) Certificate.find(filter)
.populate('user', 'name surname username') .populate('user', 'name username')
.populate('course', 'title') .populate('course', 'title')
.sort(sort) .sort(sort)
.skip(skip) .skip(skip)
+1 -1
View File
@@ -32,7 +32,7 @@ const getOne = async (id) => {
const cls = await Class.findById(id) const cls = await Class.findById(id)
.populate({ path: 'course', select: 'title type price' }) .populate({ path: 'course', select: 'title type price' })
.populate({ path: 'professor', select: 'name surname phoneNumber' }) .populate({ path: 'professor', select: 'name surname phoneNumber' })
.populate({ path: 'students', select: 'name surname phoneNumber' }) .populate({ path: 'students', select: 'name phoneNumber gender' })
.lean(); .lean();
if (!cls) throw new AppError('CLASS_NOT_FOUND'); if (!cls) throw new AppError('CLASS_NOT_FOUND');
return cls; 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);
});
+21
View File
@@ -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;
+258
View File
@@ -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 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) { if (!notification) {
throw new AppError('NOTIFICATION_NOT_FOUND'); throw new AppError('NOTIFICATION_NOT_FOUND');
} }
@@ -49,7 +49,7 @@ const getAllNotifications = async (queryParams) => {
const filter = buildFilterQuery(queryParams, ['subject', 'body']); const filter = buildFilterQuery(queryParams, ['subject', 'body']);
const [notifications, totalCount] = await Promise.all([ 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) Notification.countDocuments(filter)
]); ]);
+2 -2
View File
@@ -18,7 +18,7 @@ const getAllPayments = async (query) => {
const [items, total] = await Promise.all([ const [items, total] = await Promise.all([
Payment.find(filter) Payment.find(filter)
.populate({ path: 'user', select: 'name surname' }) .populate({ path: 'user', select: 'name' })
.populate({ path: 'classes', select: 'name' }) .populate({ path: 'classes', select: 'name' })
.populate({ path: 'course', select: 'title' }) .populate({ path: 'course', select: 'title' })
.skip(skip).limit(limit).sort({ createdAt: -1 }).lean(), .skip(skip).limit(limit).sort({ createdAt: -1 }).lean(),
@@ -30,7 +30,7 @@ const getAllPayments = async (query) => {
const getPaymentById = async (id) => { const getPaymentById = async (id) => {
const payment = await Payment.findById(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: 'classes', select: 'name tuitionFee' })
.populate({ path: 'course', select: 'title price' }) .populate({ path: 'course', select: 'title price' })
.lean(); .lean();
+2 -2
View File
@@ -85,10 +85,10 @@ const getSessionById = async (id) => {
.populate({ .populate({
path: 'class', path: 'class',
select: 'name students capacity', 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('professor', 'name surname email phoneNumber')
.populate('attendanceList.user', 'name surname username nationalIdCode'); .populate('attendanceList.user', 'name username nationalIdCode');
if (!session) { if (!session) {
throw new AppError('SESSION_NOT_FOUND'); throw new AppError('SESSION_NOT_FOUND');
} }
+29 -2
View File
@@ -21,9 +21,9 @@ const userSchema = new mongoose.Schema({
required: true, required: true,
trim: true trim: true
}, },
surname: { gender: {
type: String, type: String,
required: true, enum: ['male', 'female'],
trim: true trim: true
}, },
phoneNumber: { phoneNumber: {
@@ -51,6 +51,33 @@ const userSchema = new mongoose.Schema({
type: String, type: String,
trim: true 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: { username: {
type: String, type: String,
required: true, required: true,
+66 -39
View File
@@ -9,6 +9,7 @@ const { calculateMeta } = require('../../utils/pagination');
const { generateUsername, generateSimplePassword } = require('../../utils/credentials'); const { generateUsername, generateSimplePassword } = require('../../utils/credentials');
const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages'); const { sendAccountCreatedSms } = require('../../utils/senders/smsMessages');
const logger = require('../../utils/logger'); const logger = require('../../utils/logger');
const { mergeFullName, normalizeGender } = require('../../utils/userProfile');
const POPULATE_ROLE = { path: 'role', select: 'name permissions' }; const POPULATE_ROLE = { path: 'role', select: 'name permissions' };
const SAFE_FIELDS = '-passwordHash -refreshTokens'; 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'); 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 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' }); const userRole = await Role.findOne({ name: 'User' });
if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND'); if (!userRole) throw new AppError('DEFAULT_ROLE_NOT_FOUND');
const passwordHash = await bcrypt.hash(password, 10); const passwordHash = await bcrypt.hash(password, 10);
const user = await User.create({ const user = await User.create({
name, ...profile,
surname,
nationalIdCode: nationalIdCode || nationalId,
phoneNumber: phoneNumber || phone,
username, username,
passwordHash, passwordHash,
email,
address,
preferredMessenger: normalizePreferredMessengers(preferredMessenger),
role: userRole._id role: userRole._id
}); });
@@ -75,8 +99,9 @@ const getAllUsers = async (query) => {
if (query.search) { if (query.search) {
filter.$or = [ filter.$or = [
{ name: new RegExp(query.search, 'i') }, { 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'; if (query.isActive !== undefined) filter.isActive = query.isActive === 'true';
@@ -92,20 +117,12 @@ const getAllUsers = async (query) => {
const searchUsers = async (query) => getAllUsers(query); const searchUsers = async (query) => getAllUsers(query);
const createUserAdmin = async (body) => { const createUserAdmin = async (body) => {
const profile = pickProfileFields(body);
const { const {
name,
surname,
nationalIdCode,
nationalId,
phoneNumber,
phone,
username: requestedUsername, username: requestedUsername,
password: requestedPassword, password: requestedPassword,
email,
roleId, roleId,
role, role
address,
preferredMessenger,
} = body; } = body;
let roleObj = null; let roleObj = null;
@@ -120,25 +137,18 @@ const createUserAdmin = async (body) => {
const plainPassword = (requestedPassword && String(requestedPassword).trim()) || generateSimplePassword(); const plainPassword = (requestedPassword && String(requestedPassword).trim()) || generateSimplePassword();
const username = await allocateUniqueUsername(requestedUsername); const username = await allocateUniqueUsername(requestedUsername);
const passwordHash = await bcrypt.hash(plainPassword, 10); const passwordHash = await bcrypt.hash(plainPassword, 10);
const resolvedPhone = phoneNumber || phone;
const user = await User.create({ const user = await User.create({
name, ...profile,
surname,
nationalIdCode: nationalIdCode || nationalId,
phoneNumber: resolvedPhone,
username, username,
passwordHash, passwordHash,
email, role: roleObj._id
address,
preferredMessenger: normalizePreferredMessengers(preferredMessenger),
role: roleObj._id,
}); });
try { try {
await sendAccountCreatedSms(resolvedPhone, username, plainPassword); await sendAccountCreatedSms(profile.phoneNumber, username, plainPassword);
} catch (err) { } 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(); const created = await User.findById(user._id).select(SAFE_FIELDS).populate(POPULATE_ROLE).lean();
@@ -146,8 +156,8 @@ const createUserAdmin = async (body) => {
...created, ...created,
generatedCredentials: { generatedCredentials: {
username, username,
password: plainPassword, password: plainPassword
}, }
}; };
}; };
@@ -164,6 +174,9 @@ const updateUser = async (id, body) => {
passwordHash, passwordHash,
refreshTokens, refreshTokens,
preferredMessenger, preferredMessenger,
surname,
name,
gender,
_id, _id,
id: bodyId, id: bodyId,
createdAt, createdAt,
@@ -173,8 +186,12 @@ const updateUser = async (id, body) => {
} = body; } = body;
const update = { ...rest }; 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) { if (nationalIdCode || nationalId) {
update.nationalIdCode = nationalIdCode || nationalId; update.nationalIdCode = nationalIdCode || nationalId;
} }
@@ -188,12 +205,13 @@ const updateUser = async (id, body) => {
update.preferredMessenger = normalizePreferredMessengers(preferredMessenger) || []; update.preferredMessenger = normalizePreferredMessengers(preferredMessenger) || [];
} }
// Never overwrite username with an empty value on update
if (typeof username === 'string' && username.trim()) { if (typeof username === 'string' && username.trim()) {
update.username = 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) => { Object.keys(update).forEach((key) => {
if (key === 'preferredMessenger') return; if (key === 'preferredMessenger') return;
if (update[key] === '' || update[key] === null || update[key] === undefined) { 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'); 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); const user = await User.findById(userId);
if (!user) throw new AppError('USER_NOT_FOUND'); 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(); 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
+1 -1
View File
@@ -76,7 +76,7 @@ const activityLogger = (req, res, next) => {
actor: actor?._id || null, actor: actor?._id || null,
actorUsername: actor?.username || req.body?.username || null, actorUsername: actor?.username || req.body?.username || null,
actorName: actor actorName: actor
? `${actor.name || ''} ${actor.surname || ''}`.trim() || actor.username ? `${actor.name || ''}`.trim() || actor.username
: req.body?.username || null, : req.body?.username || null,
action, action,
resource: resolveResource(path), resource: resolveResource(path),
+100 -514
View File
@@ -12,7 +12,6 @@
"@aws-sdk/client-s3": "^3.1106.0", "@aws-sdk/client-s3": "^3.1106.0",
"@aws-sdk/s3-request-presigner": "^3.1106.0", "@aws-sdk/s3-request-presigner": "^3.1106.0",
"axios": "^1.7.9", "axios": "^1.7.9",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
@@ -25,7 +24,8 @@
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"winston": "^3.17.0" "winston": "^3.17.0",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.9" "nodemon": "^3.1.9"
@@ -391,26 +391,6 @@
"@hapi/hoek": "^9.0.0" "@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": { "node_modules/@mongodb-js/saslprep": {
"version": "1.4.13", "version": "1.4.13",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.13.tgz", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.13.tgz",
@@ -553,12 +533,6 @@
"@types/webidl-conversions": "*" "@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": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -572,6 +546,15 @@
"node": ">= 0.6" "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": { "node_modules/agent-base": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -584,15 +567,6 @@
"node": ">= 6.0.0" "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": { "node_modules/anymatch": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -613,40 +587,6 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT" "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": { "node_modules/array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -687,20 +627,6 @@
"node": "18 || 20 || >=22" "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": { "node_modules/bcryptjs": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
@@ -864,6 +790,19 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -889,13 +828,13 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/chownr": { "node_modules/codepage": {
"version": "2.0.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "ISC", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">=10" "node": ">=0.8"
} }
}, },
"node_modules/color": { "node_modules/color": {
@@ -944,15 +883,6 @@
"node": ">=18" "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": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -965,12 +895,6 @@
"node": ">= 0.8" "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": { "node_modules/concat-stream": {
"version": "1.6.2", "version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
@@ -986,12 +910,6 @@
"typedarray": "^0.0.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": { "node_modules/content-disposition": {
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -1051,6 +969,18 @@
"url": "https://opencollective.com/express" "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": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -1077,12 +1007,6 @@
"node": ">=0.4.0" "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": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -1102,15 +1026,6 @@
"npm": "1.2.8000 || >= 1.4.16" "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": { "node_modules/dotenv": {
"version": "16.6.1", "version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -1152,12 +1067,6 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT" "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": { "node_modules/enabled": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
@@ -1412,6 +1321,15 @@
"node": ">= 0.6" "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": { "node_modules/fresh": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -1421,36 +1339,6 @@
"node": ">= 0.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": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1475,27 +1363,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -1533,27 +1400,6 @@
"node": ">= 0.4" "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": { "node_modules/glob-parent": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
@@ -1567,34 +1413,6 @@
"node": ">= 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": { "node_modules/gopd": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -1644,12 +1462,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/hasown": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
@@ -1726,17 +1538,6 @@
"dev": true, "dev": true,
"license": "ISC" "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": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -1775,15 +1576,6 @@
"node": ">=0.10.0" "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": { "node_modules/is-glob": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -1955,30 +1747,6 @@
"node": ">= 12.0.0" "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": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -2079,40 +1847,6 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/mkdirp": {
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -2258,12 +1992,6 @@
"node": ">= 0.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": { "node_modules/node-cron": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
@@ -2276,48 +2004,6 @@
"node": ">=6.0.0" "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": { "node_modules/nodemailer": {
"version": "6.10.1", "version": "6.10.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
@@ -2356,21 +2042,6 @@
"url": "https://opencollective.com/nodemon" "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": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -2381,19 +2052,6 @@
"node": ">=0.10.0" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -2427,15 +2085,6 @@
"node": ">= 0.8" "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": { "node_modules/one-time": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
@@ -2454,15 +2103,6 @@
"node": ">= 0.8" "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": { "node_modules/path-to-regexp": {
"version": "0.1.13", "version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
@@ -2600,22 +2240,6 @@
"node": ">=8.10.0" "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": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -2717,12 +2341,6 @@
"node": ">= 0.8.0" "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": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -2807,12 +2425,6 @@
"integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
"license": "MIT" "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": { "node_modules/simple-update-notifier": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@@ -2835,6 +2447,18 @@
"memory-pager": "^1.0.2" "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": { "node_modules/stack-trace": {
"version": "0.0.10", "version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
@@ -2876,32 +2500,6 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT" "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": { "node_modules/supports-color": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -2915,36 +2513,6 @@
"node": ">=4" "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": { "node_modules/text-hex": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
@@ -3101,15 +2669,6 @@
"node": ">=18" "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": { "node_modules/winston": {
"version": "3.19.0", "version": "3.19.0",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
@@ -3174,11 +2733,44 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/wrappy": { "node_modules/wmf": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "ISC" "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": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
@@ -3188,12 +2780,6 @@
"engines": { "engines": {
"node": ">=0.4" "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
View File
@@ -34,7 +34,8 @@
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"node-cron": "^3.0.3", "node-cron": "^3.0.3",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"winston": "^3.17.0" "winston": "^3.17.0",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"nodemon": "^3.1.9" "nodemon": "^3.1.9"
+389
View File
@@ -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();
+1 -2
View File
@@ -146,8 +146,7 @@ const seedDatabase = async ({ disconnectOnComplete = false } = {}) => {
if (!adminUser) { if (!adminUser) {
const passwordHash = await bcrypt.hash(config.SUPERADMIN_PASSWORD, 10); const passwordHash = await bcrypt.hash(config.SUPERADMIN_PASSWORD, 10);
await User.create({ await User.create({
name: 'مدیر', name: 'مدیر ارشد',
surname: 'ارشد',
nationalIdCode: config.SUPERADMIN_NATIONAL_ID, nationalIdCode: config.SUPERADMIN_NATIONAL_ID,
phoneNumber: config.SUPERADMIN_PHONE, phoneNumber: config.SUPERADMIN_PHONE,
email: config.SUPERADMIN_EMAIL || undefined, email: config.SUPERADMIN_EMAIL || undefined,
+24
View File
@@ -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
};