const path = require('path'); const Degree = require('../models/degree'); const Department = require('../models/department'); const Level = require('../models/level'); const writeAuditLog = require('../audit/writeAuditLog'); const AUDIT_ACTIONS = require('../constants/auditAction'); // Helper: store only filename, served via /secure-files/ route function normalizePath(filePath) { if (!filePath) return undefined; return path.basename(filePath.replace(/\\/g, '/')); } // GET /admin/degree exports.index = async (req, res) => { try { const { search, type, department, level, status } = req.query; const filter = {}; if (search) { filter.$or = [ { qualification_number: { $regex: search, $options: 'i' } }, { certification_number: { $regex: search, $options: 'i' } }, { student_name: { $regex: search, $options: 'i' } } ]; } if (type) filter.type = type; if (department) filter.department = department; if (level) filter.level = level; if (status) filter.status = status; const [degrees, departments, levels] = await Promise.all([ Degree.find(filter).populate('department level').sort({ createdAt: -1 }), Department.find(), Level.find() ]); res.render('admin/degree/index', { degrees, departments, levels, query: req.query, user: req.session.user, layout: 'layouts/admin', title: 'Quản lý Văn bằng' }); } catch (err) { console.error('degreeController.index error:', err); req.flash('error', 'Đã xảy ra lỗi khi tải danh sách văn bằng'); res.redirect('/admin/dashboard'); } }; // GET /admin/degree/create exports.createForm = async (req, res) => { try { const [departments, levels] = await Promise.all([Department.find(), Level.find()]); res.render('admin/degree/create', { departments, levels, user: req.session.user, layout: 'layouts/admin', title: 'Tạo Văn bằng mới' }); } catch (err) { console.error('degreeController.createForm error:', err); req.flash('error', 'Đã xảy ra lỗi'); res.redirect('/admin/degree'); } }; // POST /admin/degree/create exports.create = async (req, res) => { try { const degreeData = { ...req.body }; const degreeImagePath = req.files?.degree_image?.[0]?.path; const certificateImagePath = req.files?.certificate_image?.[0]?.path; if (degreeImagePath) degreeData.degree_image = normalizePath(degreeImagePath); if (certificateImagePath) degreeData.certificate_image = normalizePath(certificateImagePath); const degree = new Degree(degreeData); await degree.save(); await writeAuditLog({ model: 'Degree', documentId: degree._id, action: AUDIT_ACTIONS.CREATE_DEGREE, before: null, after: degree.toObject(), req }); req.flash('success', 'Degree created'); res.redirect('/admin/degree'); } catch (err) { console.error('degreeController.create error:', err); try { const [departments, levels] = await Promise.all([Department.find(), Level.find()]); res.render('admin/degree/create', { error: err.message, formData: req.body, departments, levels, user: req.session.user, layout: 'layouts/admin', title: 'Tạo Văn bằng mới' }); } catch (renderErr) { req.flash('error', err.message); res.redirect('/admin/degree'); } } }; // GET /admin/degree/:id/edit exports.editForm = async (req, res) => { try { const degree = await Degree.findById(req.params.id).populate('department level'); if (!degree) { req.flash('error', 'Degree not found'); return res.redirect('/admin/degree'); } const [departments, levels] = await Promise.all([Department.find(), Level.find()]); res.render('admin/degree/edit', { degree, departments, levels, user: req.session.user, layout: 'layouts/admin', title: 'Chỉnh sửa Văn bằng' }); } catch (err) { console.error('degreeController.editForm error:', err); req.flash('error', 'Đã xảy ra lỗi'); res.redirect('/admin/degree'); } }; // POST /admin/degree/:id/edit exports.update = async (req, res) => { try { const degree = await Degree.findById(req.params.id); if (!degree) { req.flash('error', 'Degree not found'); return res.redirect('/admin/degree'); } const beforeData = degree.toObject(); const fields = [ 'qualification_number', 'certification_number', 'student_name', 'program_name', 'type', 'department', 'level', 'issued_date', 'status', 'passport_number', 'address', 'topic_name', 'topic_short_desc' ]; fields.forEach(field => { if (req.body[field] !== undefined) degree[field] = req.body[field]; }); const degreeImagePath = req.files?.degree_image?.[0]?.path; const certificateImagePath = req.files?.certificate_image?.[0]?.path; if (degreeImagePath) degree.degree_image = normalizePath(degreeImagePath); if (certificateImagePath) degree.certificate_image = normalizePath(certificateImagePath); await degree.save(); await writeAuditLog({ model: 'Degree', documentId: degree._id, action: AUDIT_ACTIONS.UPDATE_DEGREE, before: beforeData, after: degree.toObject(), req }); req.flash('success', 'Degree updated'); res.redirect('/admin/degree'); } catch (err) { console.error('degreeController.update error:', err); req.flash('error', err.message); res.redirect('back'); } }; // POST /admin/degree/:id/delete exports.destroy = async (req, res) => { try { const degree = await Degree.findById(req.params.id); if (!degree) { req.flash('error', 'Degree not found'); return res.redirect('/admin/degree'); } await writeAuditLog({ model: 'Degree', documentId: degree._id, action: AUDIT_ACTIONS.DELETE_DEGREE, before: degree.toObject(), after: null, req }); await degree.deleteOne(); req.flash('success', 'Degree deleted'); res.redirect('/admin/degree'); } catch (err) { console.error('degreeController.destroy error:', err); req.flash('error', 'Đã xảy ra lỗi khi xóa văn bằng'); res.redirect('/admin/degree'); } }; // ─── Public API ─────────────────────────────────────────────────────────────── function buildSecureUrl(req, filename) { if (!filename) return undefined; const baseUrl = process.env.BACKEND_URL ?? `${req.protocol}://${req.get('host')}`; const name = path.basename(filename); return `${baseUrl}/secure-files/${name}?api_key=${req.query.api_key}`; } // GET /api/verify-degree/:degree_id?api_key=xxx // Lookup by qualification_number — returns degree fields + topic_name if PhD exports.apiGetByQualification = async (req, res) => { try { const degree = await Degree.findOne({ qualification_number: { $regex: new RegExp('^' + req.params.degree_id + '$', 'i') } }).populate('department level'); if (!degree) return res.status(404).json({ error: 'Degree not found' }); if (degree.status === 'revoked') return res.status(404).json({ error: 'Degree has been revoked' }); const imageUrl = buildSecureUrl(req, degree.degree_image); const response = { full_name: degree.student_name, program_name: degree.program_name, degree_id: degree.qualification_number, }; if (degree.passport_number) response.passport_number = degree.passport_number; if (degree.address) response.address = degree.address; if (imageUrl) response.degree_image = [imageUrl]; // topic_name present → PhD view; absent → MBA/Master view if (degree.topic_name) { response.topic_name = degree.topic_name; if (degree.topic_short_desc) response.topic_short_desc = degree.topic_short_desc; } return res.json(response); } catch (err) { console.error('apiGetByQualification error:', err); return res.status(500).json({ error: 'Internal server error' }); } }; // GET /api/verify-certificate/:cert_id?api_key=xxx // Lookup by certification_number — returns certificate fields (no topic_name) exports.apiGetByCertification = async (req, res) => { try { const degree = await Degree.findOne({ certification_number: { $regex: new RegExp('^' + req.params.cert_id + '$', 'i') } }).populate('department level'); if (!degree) return res.status(404).json({ error: 'Certificate not found' }); if (degree.status === 'revoked') return res.status(404).json({ error: 'Certificate has been revoked' }); const imageUrl = buildSecureUrl(req, degree.certificate_image); const response = { full_name: degree.student_name, certification_title: degree.program_name, certificate_id: degree.certification_number, }; if (degree.passport_number) response.passport_number = degree.passport_number; if (degree.address) response.address = degree.address; if (imageUrl) response.certificate_image = [imageUrl]; return res.json(response); } catch (err) { console.error('apiGetByCertification error:', err); return res.status(500).json({ error: 'Internal server error' }); } };