diff --git a/controllers/certificateController.js b/controllers/certificateController.js index 7d39082..0d678df 100644 --- a/controllers/certificateController.js +++ b/controllers/certificateController.js @@ -57,8 +57,8 @@ exports.createForm = async (req, res) => { exports.create = async (req, res) => { try { const data = { ...req.body }; - const imgPath = req.files?.certificate_image?.[0]?.path; - if (imgPath) data.certificate_image = normalizePath(imgPath); + const imgFiles = req.files?.certificate_image; + if (imgFiles?.length) data.certificate_image = imgFiles.map(f => normalizePath(f.path)); const cert = new Certificate(data); await cert.save(); @@ -104,8 +104,8 @@ exports.update = async (req, res) => { 'issued_date','status','passport_number','address']; fields.forEach(f => { if (req.body[f] !== undefined) cert[f] = req.body[f]; }); - const imgPath = req.files?.certificate_image?.[0]?.path; - if (imgPath) cert.certificate_image = normalizePath(imgPath); + const imgFiles = req.files?.certificate_image; + if (imgFiles?.length) cert.certificate_image = imgFiles.map(f => normalizePath(f.path)); await cert.save(); await writeAuditLog({ model: 'Certificate', documentId: cert._id, action: AUDIT_ACTIONS.UPDATE_CERTIFICATE, before, after: cert.toObject(), req }); @@ -142,7 +142,10 @@ exports.apiVerify = async (req, res) => { if (cert.status === 'revoked') return res.status(404).json({ error: 'Certificate has been revoked' }); const baseUrl = process.env.BACKEND_URL ?? `${req.protocol}://${req.get('host')}`; - const buildUrl = (f) => f ? [generateSignedUrl(baseUrl, path.basename(f))] : undefined; + const buildUrl = (files) => { + if (!files?.length) return undefined; + return files.map(f => generateSignedUrl(baseUrl, path.basename(f))); + }; const response = { full_name: cert.student_name, diff --git a/controllers/qualificationController.js b/controllers/qualificationController.js index 296df1f..dd7ea7e 100644 --- a/controllers/qualificationController.js +++ b/controllers/qualificationController.js @@ -57,8 +57,8 @@ exports.createForm = async (req, res) => { exports.create = async (req, res) => { try { const data = { ...req.body }; - const imgPath = req.files?.degree_image?.[0]?.path; - if (imgPath) data.degree_image = normalizePath(imgPath); + const imgFiles = req.files?.degree_image; + if (imgFiles?.length) data.degree_image = imgFiles.map(f => normalizePath(f.path)); const qual = new Qualification(data); await qual.save(); @@ -104,8 +104,8 @@ exports.update = async (req, res) => { 'issued_date','status','passport_number','address','topic_name','topic_short_desc']; fields.forEach(f => { if (req.body[f] !== undefined) qual[f] = req.body[f]; }); - const imgPath = req.files?.degree_image?.[0]?.path; - if (imgPath) qual.degree_image = normalizePath(imgPath); + const imgFiles = req.files?.degree_image; + if (imgFiles?.length) qual.degree_image = imgFiles.map(f => normalizePath(f.path)); await qual.save(); await writeAuditLog({ model: 'Qualification', documentId: qual._id, action: AUDIT_ACTIONS.UPDATE_QUALIFICATION, before, after: qual.toObject(), req }); @@ -142,7 +142,10 @@ exports.apiVerify = async (req, res) => { if (qual.status === 'revoked') return res.status(404).json({ error: 'Degree has been revoked' }); const baseUrl = process.env.BACKEND_URL ?? `${req.protocol}://${req.get('host')}`; - const buildUrl = (f) => f ? [generateSignedUrl(baseUrl, path.basename(f))] : undefined; + const buildUrl = (files) => { + if (!files?.length) return undefined; + return files.map(f => generateSignedUrl(baseUrl, path.basename(f))); + }; const response = { full_name: qual.student_name, diff --git a/middleware/upload.js b/middleware/upload.js index 260864d..77fdc4b 100644 --- a/middleware/upload.js +++ b/middleware/upload.js @@ -170,24 +170,24 @@ const degreeStorage = multer.diskStorage({ } }); -// Lọc file chỉ cho phép ảnh degree +// Lọc file cho phép ảnh và PDF const degreeFileFilter = (req, file, cb) => { - const allowedMimes = ['image/jpeg', 'image/png', 'image/webp']; + const allowedMimes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf']; if (allowedMimes.includes(file.mimetype)) { cb(null, true); } else { - cb(new Error('Only image/jpeg, image/png, image/webp files are allowed!')); + cb(new Error('Only image/jpeg, image/png, image/webp, application/pdf files are allowed!')); } }; -// Cấu hình upload degree +// Cấu hình upload degree — nhiều file, hỗ trợ PDF const uploadDegree = multer({ storage: degreeStorage, - limits: { fileSize: 5 * 1024 * 1024 }, // 5MB per file + limits: { fileSize: 20 * 1024 * 1024 }, // 20MB per file fileFilter: degreeFileFilter }).fields([ - { name: 'degree_image', maxCount: 1 }, - { name: 'certificate_image', maxCount: 1 } + { name: 'degree_image', maxCount: 10 }, + { name: 'certificate_image', maxCount: 10 } ]); module.exports = { diff --git a/models/certificate.js b/models/certificate.js index 89a0c9e..4e50659 100644 --- a/models/certificate.js +++ b/models/certificate.js @@ -25,8 +25,8 @@ const certificateSchema = new mongoose.Schema({ // Optional personal info passport_number: { type: String, trim: true }, address: { type: String, trim: true }, - // Document image - certificate_image: { type: String } + // Document images (array of filenames) + certificate_image: { type: [String], default: [] } }, { timestamps: true }); module.exports = mongoose.model('Certificate', certificateSchema); diff --git a/models/qualification.js b/models/qualification.js index 6dd98cd..616e938 100644 --- a/models/qualification.js +++ b/models/qualification.js @@ -28,8 +28,8 @@ const qualificationSchema = new mongoose.Schema({ // PhD fields — presence of topic_name signals PhD view on frontend topic_name: { type: String, trim: true }, topic_short_desc: { type: String, trim: true }, - // Document image - degree_image: { type: String } + // Document images (array of filenames) + degree_image: { type: [String], default: [] } }, { timestamps: true }); module.exports = mongoose.model('Qualification', qualificationSchema); diff --git a/views/admin/certificate/create.ejs b/views/admin/certificate/create.ejs index 745904a..6522571 100644 --- a/views/admin/certificate/create.ejs +++ b/views/admin/certificate/create.ejs @@ -91,9 +91,10 @@