From 43bfc117bf83262d0b496c2903f83def4ddf2a63 Mon Sep 17 00:00:00 2001 From: nthanhtoan61 Date: Wed, 15 Apr 2026 16:55:32 +0700 Subject: [PATCH] upload pdf --- controllers/certificateController.js | 13 ++++++++----- controllers/qualificationController.js | 13 ++++++++----- middleware/upload.js | 14 +++++++------- models/certificate.js | 4 ++-- models/qualification.js | 4 ++-- views/admin/certificate/create.ejs | 5 +++-- views/admin/certificate/edit.ejs | 22 ++++++++++++++++------ views/admin/qualification/create.ejs | 5 +++-- views/admin/qualification/edit.ejs | 22 ++++++++++++++++------ 9 files changed, 65 insertions(+), 37 deletions(-) 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 @@
-
Certificate Image
+
Certificate Documents
- + +
Accepted: JPG, PNG, WEBP, PDF — max 20MB each. You can select multiple files.
diff --git a/views/admin/certificate/edit.ejs b/views/admin/certificate/edit.ejs index 4af2b51..8d8417d 100644 --- a/views/admin/certificate/edit.ejs +++ b/views/admin/certificate/edit.ejs @@ -85,15 +85,25 @@
-
Certificate Image
+
Certificate Documents
- <% if (cert.certificate_image) { %> -
- Certificate image + <% if (cert.certificate_image && cert.certificate_image.length) { %> +
+ <% cert.certificate_image.forEach(function(f) { %> + <% if (f.match(/\.(pdf)$/i)) { %> + + <%= f %> + + <% } else { %> + + certificate + + <% } %> + <% }); %>
<% } %> - -
Leave empty to keep current image.
+ +
Accepted: JPG, PNG, WEBP, PDF — max 20MB each. Uploading new files will replace existing ones.
diff --git a/views/admin/qualification/create.ejs b/views/admin/qualification/create.ejs index 2032fb8..5ba8245 100644 --- a/views/admin/qualification/create.ejs +++ b/views/admin/qualification/create.ejs @@ -108,9 +108,10 @@
-
Degree Image
+
Degree Documents
- + +
Accepted: JPG, PNG, WEBP, PDF — max 20MB each. You can select multiple files.
diff --git a/views/admin/qualification/edit.ejs b/views/admin/qualification/edit.ejs index 00c741d..36ae7ce 100644 --- a/views/admin/qualification/edit.ejs +++ b/views/admin/qualification/edit.ejs @@ -101,15 +101,25 @@
-
Degree Image
+
Degree Documents
- <% if (qual.degree_image) { %> -
- Degree image + <% if (qual.degree_image && qual.degree_image.length) { %> +
+ <% qual.degree_image.forEach(function(f) { %> + <% if (f.match(/\.(pdf)$/i)) { %> + + <%= f %> + + <% } else { %> + + degree + + <% } %> + <% }); %>
<% } %> - -
Leave empty to keep current image.
+ +
Accepted: JPG, PNG, WEBP, PDF — max 20MB each. Uploading new files will replace existing ones.