campverse-backend / Controller /certificateManagement.js
CampVerse CI
Deploy backend from GitHub Actions
37b442c
Raw History Blame Contribute Delete
26.8 kB
const Event = require('../Models/Event');
const EventParticipationLog = require('../Models/EventParticipationLog');
const Certificate = require('../Models/Certificate');
const User = require('../Models/User');
const axios = require('axios');
const { storageService } = require('../Services/storageService');
// Certificate Generator API URL (update based on your deployment)
const CERTIFICATE_API_URL = process.env.CERTIFICATE_API_URL || 'http://localhost:8000';
/**
* Update event certificate settings
* Host can enable/disable certificates and configure settings
*/
async function updateCertificateSettings(req, res) {
try {
const { eventId } = req.params;
const userId = req.user.id;
const {
certificateEnabled,
certificateType,
awardText,
leftSignatory,
rightSignatory,
selectedTemplateId, // Template ID assigned by Admin/Verifier
previewConfig,
} = req.body;
// Find event and verify host permission
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ error: 'Event not found.' });
}
const isHost = event.hostUserId.toString() === userId;
if (!isHost) {
return res.status(403).json({
error: 'Only the event host can update certificate settings.',
});
}
// Update certificate settings
if (certificateEnabled !== undefined) {
event.features = event.features || {};
event.features.certificateEnabled = certificateEnabled;
}
if (!event.certificateSettings) {
event.certificateSettings = {};
}
if (certificateType) {
event.certificateSettings.certificateType = certificateType;
}
if (awardText !== undefined) {
event.certificateSettings.awardText = awardText;
}
if (leftSignatory) {
event.certificateSettings.leftSignatory = leftSignatory;
}
if (rightSignatory) {
event.certificateSettings.rightSignatory = rightSignatory;
}
if (selectedTemplateId) {
event.certificateSettings.selectedTemplateId = selectedTemplateId;
}
if (previewConfig !== undefined) {
event.certificateSettings.previewConfig = previewConfig;
}
await event.save();
res.json({
message: 'Certificate settings updated successfully.',
certificateEnabled: event.features.certificateEnabled,
certificateSettings: event.certificateSettings,
});
} catch (err) {
console.error('Error updating certificate settings:', err);
res.status(500).json({ error: 'Server error updating certificate settings.' });
}
}
/**
* Upload certificate assets (template, logos, signatures)
* Files are uploaded to cloud storage
*/
async function uploadCertificateAssets(req, res) {
try {
const { eventId } = req.params;
const userId = req.user.id;
const { assetType } = req.body; // 'template', 'orgLogo', 'leftSignature', 'rightSignature'
// Verify event and host permission
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ error: 'Event not found.' });
}
const isHost = event.hostUserId.toString() === userId;
if (!isHost) {
return res.status(403).json({
error: 'Only the event host can upload certificate assets.',
});
}
// Check if certificates are enabled
if (!event.features?.certificateEnabled) {
return res.status(400).json({
error: 'Certificates are not enabled for this event.',
});
}
// Check if files were uploaded
if (!req.files || (Array.isArray(req.files) && req.files.length === 0)) {
return res.status(400).json({ error: 'No files uploaded.' });
}
// DISALLOW hosts from uploading templates (requested: Centralized Template Management)
if (assetType === 'template' && req.user.role !== 'admin' && req.user.role !== 'platformAdmin') {
return res.status(403).json({
error: 'Only administrators can upload custom certificate templates. Please select from the predefined templates.'
});
}
const uploadedFiles = [];
// Initialize certificate assets structure
if (!event.certificateSettings) {
event.certificateSettings = {};
}
if (!event.certificateSettings.uploadedAssets) {
event.certificateSettings.uploadedAssets = {};
}
// Upload files to cloud storage
for (const file of req.files) {
try {
// Normalize assetType from frontend
let effectiveAssetType = assetType;
if (assetType === 'logo') {
effectiveAssetType = req.body.logo_type === 'right' ? 'rightLogo' : 'orgLogo';
} else if (assetType === 'signature') {
effectiveAssetType = req.body.signature_type === 'right' ? 'rightSignature' : 'leftSignature';
}
switch (effectiveAssetType) {
case 'template':
const templateType = req.body.template_type || 'participation';
uploadResult = await storageService.uploadCertificateTemplate(
file.buffer,
file.originalname,
eventId,
templateType,
file.mimetype
);
// Store template info
event.certificateSettings.uploadedAssets.templateUrl = uploadResult.url;
event.certificateSettings.uploadedAssets.templatePath = uploadResult.path;
event.certificateSettings.certificateType = templateType;
break;
case 'orgLogo':
case 'leftLogo':
uploadResult = await storageService.uploadCertificateLogo(
file.buffer,
file.originalname,
eventId,
'left',
file.mimetype
);
// Store org logo info
event.certificateSettings.uploadedAssets.organizationLogo = uploadResult.url;
event.certificateSettings.uploadedAssets.orgLogoPath = uploadResult.path;
break;
case 'rightLogo':
uploadResult = await storageService.uploadCertificateLogo(
file.buffer,
file.originalname,
eventId,
'right',
file.mimetype
);
// Store right logo info
event.certificateSettings.uploadedAssets.rightLogoUrl = uploadResult.url;
event.certificateSettings.uploadedAssets.rightLogoPath = uploadResult.path;
break;
case 'leftSignature':
uploadResult = await storageService.uploadCertificateSignature(
file.buffer,
file.originalname,
eventId,
'left',
file.mimetype
);
// Store left signature info
if (!event.certificateSettings.leftSignatory) {
event.certificateSettings.leftSignatory = {};
}
event.certificateSettings.leftSignatory.signatureUrl = uploadResult.url;
event.certificateSettings.leftSignatory.signaturePath = uploadResult.path;
break;
case 'rightSignature':
uploadResult = await storageService.uploadCertificateSignature(
file.buffer,
file.originalname,
eventId,
'right',
file.mimetype
);
// Store right signature info
if (!event.certificateSettings.rightSignatory) {
event.certificateSettings.rightSignatory = {};
}
event.certificateSettings.rightSignatory.signatureUrl = uploadResult.url;
event.certificateSettings.rightSignatory.signaturePath = uploadResult.path;
break;
default:
console.warn(`[Cert] Unknown effective asset type: ${effectiveAssetType}`);
continue;
}
uploadedFiles.push({
originalName: file.originalname,
assetType,
url: uploadResult.url,
path: uploadResult.path,
});
} catch (uploadError) {
console.error('Error uploading asset:', uploadError.message);
throw new Error(`Failed to upload ${file.originalname}: ${uploadError.message}`);
}
}
// Save event with updated assets
await event.save();
res.json({
message: 'Assets uploaded successfully to cloud storage.',
uploadedFiles,
certificateSettings: event.certificateSettings,
});
} catch (err) {
console.error('Error uploading certificate assets:', err);
res.status(500).json({ error: err.message || 'Server error uploading assets.' });
}
}
/**
* Generate certificates for all attended participants
* This is triggered by the host after the event
*/
async function generateCertificates(req, res) {
try {
const { eventId } = req.params;
const userId = req.user.id;
// Verify event and host permission
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ error: 'Event not found.' });
}
const isHost = event.hostUserId.toString() === userId;
if (!isHost) {
return res.status(403).json({
error: 'Only the event host can generate certificates.',
});
}
// Check if certificates are enabled
if (!event.features?.certificateEnabled) {
return res.status(400).json({
error: 'Certificates are not enabled for this event.',
});
}
// Check if certificate setup is approved
if (event.certificateSettings?.verificationStatus !== 'approved') {
return res.status(400).json({
error: 'Certificate setup must be approved by a verifier before generation.',
currentStatus: event.certificateSettings?.verificationStatus || 'not_configured',
});
}
// Check if required assets are uploaded - using uploadedAssets structure
const uploadedAssets = event.certificateSettings?.uploadedAssets || {};
const settings = event.certificateSettings || {};
// Template can come from upload OR predefined selection
const PREDEFINED_TEMPLATES = {
'classic-blue': 'https://storage.campverse.com/templates/classic-blue.png',
'modern-purple': 'https://storage.campverse.com/templates/modern-purple.png',
'elegant-gold': 'https://storage.campverse.com/templates/elegant-gold.png',
'minimal-dark': 'https://storage.campverse.com/templates/minimal-dark.png',
};
const templateUrl = uploadedAssets.templateUrl || PREDEFINED_TEMPLATES[settings.selectedTemplateId];
if (!templateUrl || !uploadedAssets.organizationLogo ||
!uploadedAssets.leftSignature || !uploadedAssets.rightSignature) {
return res.status(400).json({
error: 'All required assets must be uploaded (template/selection, organization logo, and both signatures).',
uploaded: {
template: !!templateUrl,
orgLogo: !!uploadedAssets.organizationLogo,
leftSignature: !!uploadedAssets.leftSignature,
rightSignature: !!uploadedAssets.rightSignature,
},
});
}
// Get all attended participants
const attendedLogs = await EventParticipationLog.find({
eventId,
status: 'attended',
}).populate('userId', 'name email');
if (attendedLogs.length === 0) {
return res.status(400).json({
error: 'No attended participants found for this event.',
});
}
// Prepare batch generation request - use the corrected variables
const certificateSettings = event.certificateSettings;
const batchRequest = {
eventId: eventId,
eventTitle: event.title,
templateUrl: templateUrl, // Already resolved above with fallback
orgLogoUrl: uploadedAssets.organizationLogo,
leftSignature: {
url: uploadedAssets.leftSignature,
name: certificateSettings.leftSignatory?.name || '',
title: certificateSettings.leftSignatory?.title || '',
},
rightSignature: {
url: uploadedAssets.rightSignature,
name: certificateSettings.rightSignatory?.name || '',
title: certificateSettings.rightSignatory?.title || '',
},
awardText: certificateSettings.awardText || 'For successfully participating in',
certificateType: certificateSettings.certificateType || 'participation',
participants: attendedLogs.map((log) => ({
userId: log.userId._id.toString(),
name: log.userId.name,
email: log.userId.email,
})),
};
// Call ML batch generation endpoint - NOT storing PDFs, just validating generation
// Certificates will be rendered on-demand when requested
let generateResponse;
try {
// Send a test generation request to validate assets
generateResponse = await axios.post(
`${CERTIFICATE_API_URL}/batch-validate`,
batchRequest,
{ timeout: 30000 } // 30 second timeout just for validation
);
} catch (apiError) {
console.error('Error validating certificate generation:', apiError.message);
return res.status(500).json({
error: 'Failed to validate certificate generation from ML service.',
details: apiError.message,
});
}
// Create certificate records with metadata only (NO URLs stored)
// Certificates will be generated on-demand when users request them
const certificateRecords = [];
for (const log of attendedLogs) {
// Check if certificate record already exists
let certificate = await Certificate.findOne({
userId: log.userId._id,
eventId: eventId,
});
if (!certificate) {
certificate = await Certificate.create({
userId: log.userId._id,
eventId: eventId,
type: certificateSettings.certificateType === 'achievement' ? 'winner' : 'participant', // REQUIRED field
status: 'ready', // Status indicates certificate is ready to be rendered
issuedAt: new Date(),
// NO certificateUrl - certificates are rendered on-demand
});
} else {
// Update existing certificate
certificate.status = 'ready';
certificate.updatedAt = new Date();
await certificate.save();
}
certificateRecords.push({
userId: log.userId._id,
userName: log.userId.name,
email: log.userId.email,
status: 'ready',
renderUrl: `/api/certificate-management/events/${eventId}/render/${log.userId._id}`, // Correct path
});
}
// Update event with generation timestamp
event.certificateSettings.lastGeneratedAt = new Date();
event.certificateSettings.certificatesReady = true;
await event.save();
res.json({
message: `Successfully prepared ${certificateRecords.length} certificate(s) for on-demand rendering.`,
note: 'Certificates are not stored. They will be generated when users download them.',
certificates: certificateRecords,
totalGenerated: certificateRecords.length,
totalParticipants: attendedLogs.length,
});
} catch (err) {
console.error('Error generating certificates:', err);
res.status(500).json({
error: 'Server error generating certificates.',
details: err.message,
});
}
}
/**
* Get certificate status for an event
* Returns list of participants and their certificate status
*/
async function getCertificateStatus(req, res) {
try {
const { eventId } = req.params;
const userId = req.user.id;
// Verify event and host permission
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ error: 'Event not found.' });
}
const isHost = event.hostUserId.toString() === userId;
const isCoHost = event.coHosts?.some((id) => id.toString() === userId);
if (!isHost && !isCoHost) {
return res.status(403).json({
error: 'Only host or co-host can view certificate status.',
});
}
// Get all attended participants
const attendedLogs = await EventParticipationLog.find({
eventId,
status: 'attended',
}).populate('userId', 'name email');
// Get certificate records
const certificates = await Certificate.find({ eventId });
const certificateMap = {};
certificates.forEach((cert) => {
certificateMap[cert.userId.toString()] = cert;
});
// Build status list
const statusList = attendedLogs.map((log) => {
const cert = certificateMap[log.userId._id.toString()];
return {
userId: log.userId._id,
name: log.userId.name,
email: log.userId.email,
attendedAt: log.attendanceTimestamp,
certificateGenerated: !!cert,
certificateUrl: cert?.certificateUrl,
certificateStatus: cert?.status,
generatedAt: cert?.generatedAt,
};
});
res.json({
eventTitle: event.title,
certificateEnabled: event.features?.certificateEnabled || false,
totalAttended: attendedLogs.length,
certificatesGenerated: certificates.length,
participants: statusList,
});
} catch (err) {
console.error('Error fetching certificate status:', err);
res.status(500).json({ error: 'Server error fetching certificate status.' });
}
}
/**
* Get user's certificate for a specific event
*/
async function getUserCertificate(req, res) {
try {
const { eventId } = req.params;
const userId = req.user.id;
// Check if user attended the event
const log = await EventParticipationLog.findOne({
eventId,
userId,
status: 'attended',
});
if (!log) {
return res.status(404).json({
error: 'Certificate not available.',
message: 'You must attend the event to receive a certificate.',
});
}
// Find certificate
const certificate = await Certificate.findOne({
eventId,
userId,
}).populate('eventId', 'title date');
if (!certificate) {
return res.status(404).json({
error: 'Certificate not generated yet.',
message: 'The event host has not generated certificates yet.',
});
}
res.json({
certificate: {
certificateId: certificate._id,
eventTitle: certificate.eventId.title,
eventDate: certificate.eventId.date,
certificateUrl: certificate.certificateUrl,
generatedAt: certificate.generatedAt,
status: certificate.status,
},
});
} catch (err) {
console.error('Error fetching user certificate:', err);
res.status(500).json({ error: 'Server error fetching certificate.' });
}
}
/**
* Regenerate certificates (if needed)
*/
async function regenerateCertificates(req, res) {
try {
const { eventId } = req.params;
const userId = req.user.id;
// Verify event and host permission
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ error: 'Event not found.' });
}
const isHost = event.hostUserId.toString() === userId;
if (!isHost) {
return res.status(403).json({
error: 'Only the event host can regenerate certificates.',
});
}
// Delete existing certificates
await Certificate.deleteMany({ eventId });
// Regenerate
return generateCertificates(req, res);
} catch (err) {
console.error('Error regenerating certificates:', err);
res.status(500).json({ error: 'Server error regenerating certificates.' });
}
}
/**
* Bulk upload participant data via CSV for certificate generation
*/
async function bulkUploadParticipants(req, res) {
try {
const { eventId } = req.params;
const userId = req.user.id;
// Verify event and host permission
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ error: 'Event not found.' });
}
const isHost = event.hostUserId.toString() === userId;
if (!isHost) {
return res.status(403).json({
error: 'Only the event host can upload participant data.',
});
}
// This endpoint expects a CSV file with participant data
// The CSV should be parsed and used to update/verify participant records
// Implementation depends on your CSV parsing library
res.json({
message: 'Bulk upload functionality - to be implemented with CSV parser.',
});
} catch (err) {
console.error('Error in bulk upload:', err);
res.status(500).json({ error: 'Server error in bulk upload.' });
}
}
/**
* Render certificate on-demand for a specific user
* Generates the certificate in real-time and streams it to the response
*/
async function renderCertificate(req, res) {
try {
const { eventId, userId } = req.params;
const requestingUserId = req.user.id;
// Check if user is requesting their own certificate or is the host
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ error: 'Event not found.' });
}
const isHost = event.hostUserId.toString() === requestingUserId;
const isOwnCertificate = userId === requestingUserId;
const isVerifier = req.user.roles.includes('verifier');
const isAdmin = req.user.roles.includes('platformAdmin');
if (!isHost && !isOwnCertificate && !isVerifier && !isAdmin) {
return res.status(403).json({
error: 'You can only access your own certificate or certificates from events you host.',
});
}
// Check if certificate is ready
const certificate = await Certificate.findOne({
eventId,
userId,
status: 'ready',
});
if (!certificate) {
return res.status(404).json({
error: 'Certificate not available.',
message: 'Certificate has not been generated yet or you did not attend this event.',
});
}
// Get user details
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found.' });
}
// Get certificate settings
const certificateSettings = event.certificateSettings;
const uploadedAssets = certificateSettings?.uploadedAssets || {};
// Template can come from upload OR predefined selection
const PREDEFINED_TEMPLATES = {
'classic-blue': 'https://storage.campverse.com/templates/classic-blue.png',
'modern-purple': 'https://storage.campverse.com/templates/modern-purple.png',
'elegant-gold': 'https://storage.campverse.com/templates/elegant-gold.png',
'minimal-dark': 'https://storage.campverse.com/templates/minimal-dark.png',
};
const templateUrl = uploadedAssets.templateUrl || PREDEFINED_TEMPLATES[certificateSettings?.selectedTemplateId];
if (!templateUrl) {
return res.status(500).json({
error: 'Certificate template not configured.',
});
}
// Prepare single certificate generation request
const renderRequest = {
eventId: eventId,
eventTitle: event.title,
templateUrl: templateUrl,
orgLogoUrl: uploadedAssets.organizationLogo,
rightLogoUrl: uploadedAssets.rightLogoUrl || 'https://campverse.com/logo.png', // Default CampVerse logo
leftSignature: {
url: uploadedAssets.leftSignature,
name: certificateSettings.leftSignatory?.name || '',
title: certificateSettings.leftSignatory?.title || '',
},
rightSignature: {
url: uploadedAssets.rightSignature,
name: certificateSettings.rightSignatory?.name || '',
title: certificateSettings.rightSignatory?.title || '',
},
awardText: certificateSettings.awardText || 'For successfully participating in',
certificateType: certificateSettings.certificateType || 'participation',
participant: {
userId: userId,
name: user.name,
email: user.email,
},
};
// Call ML single certificate render endpoint
try {
const renderResponse = await axios.post(
`${CERTIFICATE_API_URL}/render-certificate`,
renderRequest,
{
responseType: 'arraybuffer', // Get PDF as binary data
timeout: 30000, // 30 second timeout
}
);
// Stream the PDF directly to the response
res.setHeader('Content-Type', 'application/pdf');
res.setHeader(
'Content-Disposition',
`attachment; filename="${user.name.replace(/[^a-zA-Z0-9]/g, '_')}_Certificate.pdf"`
);
res.send(renderResponse.data);
} catch (apiError) {
console.error('Error rendering certificate:', apiError.message);
return res.status(500).json({
error: 'Failed to render certificate.',
details: apiError.message,
});
}
} catch (err) {
console.error('Error in renderCertificate:', err);
res.status(500).json({
error: 'Server error rendering certificate.',
details: err.message,
});
}
}
/**
* Submit certificate configuration for verification
*/
async function submitCertificateConfig(req, res) {
try {
const { eventId } = req.params;
const userId = req.user.id;
// Find event and verify host permission
const event = await Event.findById(eventId);
if (!event) {
return res.status(404).json({ error: 'Event not found.' });
}
const isHost = event.hostUserId.toString() === userId;
if (!isHost) {
return res.status(403).json({
error: 'Only the event host can submit certificate configuration.',
});
}
// Check if certificates are enabled
if (!event.features?.certificateEnabled) {
return res.status(400).json({
error: 'Certificates are not enabled for this event.',
});
}
// Validate if basic settings exist
const settings = event.certificateSettings;
if (!settings?.awardText || !settings?.leftSignatory?.name || !settings?.rightSignatory?.name) {
return res.status(400).json({
error: 'Please configure Award Text and both Signatories before submitting.',
});
}
// Update status
event.certificateSettings.verificationStatus = 'pending';
event.certificateSettings.submittedBy = userId;
event.certificateSettings.submittedAt = new Date();
await event.save();
res.json({
message: 'Certificate configuration submitted for verification.',
verificationStatus: 'pending',
});
} catch (err) {
console.error('Error submitting certificate configuration:', err);
res.status(500).json({ error: 'Server error submitting configuration.' });
}
}
module.exports = {
updateCertificateSettings,
uploadCertificateAssets,
generateCertificates,
getCertificateStatus,
getUserCertificate,
regenerateCertificates,
bulkUploadParticipants,
renderCertificate,
submitCertificateConfig,
};