upload pdf

This commit is contained in:
2026-04-15 16:55:32 +07:00
parent 50332f2548
commit 43bfc117bf
9 changed files with 65 additions and 37 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 = {

View File

@@ -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);

View File

@@ -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);

View File

@@ -91,9 +91,10 @@
<div class="col-lg-4">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Certificate Image</h5></div>
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-file-upload"></i> Certificate Documents</h5></div>
<div class="card-body">
<input type="file" class="form-control" name="certificate_image" accept="image/*">
<input type="file" class="form-control" name="certificate_image" accept="image/*,application/pdf" multiple>
<div class="form-text">Accepted: JPG, PNG, WEBP, PDF — max 20MB each. You can select multiple files.</div>
</div>
</div>
<div class="card border-0">

View File

@@ -85,15 +85,25 @@
<div class="col-lg-4">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Certificate Image</h5></div>
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-file-upload"></i> Certificate Documents</h5></div>
<div class="card-body">
<% if (cert.certificate_image) { %>
<div class="mb-2">
<img src="/admin/files/<%= cert.certificate_image %>" alt="Certificate image" class="img-thumbnail" style="max-height:110px;border-radius:var(--border-radius-sm);">
<% if (cert.certificate_image && cert.certificate_image.length) { %>
<div class="mb-3 d-flex flex-wrap gap-2">
<% cert.certificate_image.forEach(function(f) { %>
<% if (f.match(/\.(pdf)$/i)) { %>
<a href="/admin/files/<%= f %>" target="_blank" class="btn btn-sm btn-outline-danger">
<i class="fas fa-file-pdf"></i> <%= f %>
</a>
<% } else { %>
<a href="/admin/files/<%= f %>" target="_blank">
<img src="/admin/files/<%= f %>" alt="certificate" class="img-thumbnail" style="max-height:90px;border-radius:var(--border-radius-sm);">
</a>
<% } %>
<% }); %>
</div>
<% } %>
<input type="file" class="form-control" name="certificate_image" accept="image/*">
<div class="form-text">Leave empty to keep current image.</div>
<input type="file" class="form-control" name="certificate_image" accept="image/*,application/pdf" multiple>
<div class="form-text">Accepted: JPG, PNG, WEBP, PDF — max 20MB each. Uploading new files will replace existing ones.</div>
</div>
</div>
<div class="card border-0">

View File

@@ -108,9 +108,10 @@
<div class="col-lg-4">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Degree Image</h5></div>
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-file-upload"></i> Degree Documents</h5></div>
<div class="card-body">
<input type="file" class="form-control" name="degree_image" accept="image/*">
<input type="file" class="form-control" name="degree_image" accept="image/*,application/pdf" multiple>
<div class="form-text">Accepted: JPG, PNG, WEBP, PDF — max 20MB each. You can select multiple files.</div>
</div>
</div>
<div class="card border-0">

View File

@@ -101,15 +101,25 @@
<div class="col-lg-4">
<div class="card border-0 mb-3">
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-image"></i> Degree Image</h5></div>
<div class="card-header"><h5 class="card-header-title"><i class="fas fa-file-upload"></i> Degree Documents</h5></div>
<div class="card-body">
<% if (qual.degree_image) { %>
<div class="mb-2">
<img src="/admin/files/<%= qual.degree_image %>" alt="Degree image" class="img-thumbnail" style="max-height:110px;border-radius:var(--border-radius-sm);">
<% if (qual.degree_image && qual.degree_image.length) { %>
<div class="mb-3 d-flex flex-wrap gap-2">
<% qual.degree_image.forEach(function(f) { %>
<% if (f.match(/\.(pdf)$/i)) { %>
<a href="/admin/files/<%= f %>" target="_blank" class="btn btn-sm btn-outline-danger">
<i class="fas fa-file-pdf"></i> <%= f %>
</a>
<% } else { %>
<a href="/admin/files/<%= f %>" target="_blank">
<img src="/admin/files/<%= f %>" alt="degree" class="img-thumbnail" style="max-height:90px;border-radius:var(--border-radius-sm);">
</a>
<% } %>
<% }); %>
</div>
<% } %>
<input type="file" class="form-control" name="degree_image" accept="image/*">
<div class="form-text">Leave empty to keep current image.</div>
<input type="file" class="form-control" name="degree_image" accept="image/*,application/pdf" multiple>
<div class="form-text">Accepted: JPG, PNG, WEBP, PDF — max 20MB each. Uploading new files will replace existing ones.</div>
</div>
</div>
<div class="card border-0">