forked from UKSOURCE/cms.hailearning.edu.vn
upload pdf
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user