feat: add endpoints for admin and authenticated user to preview latest active contracts with DB-filled placeholders
This commit is contained in:
parent
4fc78f3f8b
commit
40a7af56b1
@ -1277,6 +1277,244 @@ exports.previewPdf = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// NEW: Admin-only endpoint to preview latest active contract for a specific user with DB-filled placeholders
|
||||||
|
// GET /api/admin/contracts/:id/preview?userType=personal|company&type=contract
|
||||||
|
exports.previewLatestForUser = async (req, res) => {
|
||||||
|
const targetUserId = parseInt(req.params.id, 10);
|
||||||
|
const overrideUserType = (req.query.userType || req.query.user_type || '').toString().toLowerCase();
|
||||||
|
const templateType = (req.query.type || 'contract').toString();
|
||||||
|
|
||||||
|
if (!req.user || !['admin', 'super_admin'].includes(req.user.role)) {
|
||||||
|
return res.status(403).json({ error: 'Forbidden: Admins only' });
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(targetUserId) || targetUserId <= 0) {
|
||||||
|
return res.status(400).json({ error: 'Invalid user id' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Resolve target user and determine user_type if not overridden
|
||||||
|
let userRow = null;
|
||||||
|
try {
|
||||||
|
const [uRows] = await db.execute('SELECT id, email, user_type, role FROM users WHERE id = ? LIMIT 1', [targetUserId]);
|
||||||
|
userRow = (uRows && uRows[0]) ? uRows[0] : null;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('[previewLatestForUser] failed to load users row', e && e.message);
|
||||||
|
}
|
||||||
|
if (!userRow) {
|
||||||
|
return res.status(404).json({ error: 'User not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userType = (overrideUserType === 'personal' || overrideUserType === 'company') ? overrideUserType : userRow.user_type;
|
||||||
|
|
||||||
|
// Find the latest active template for this user type
|
||||||
|
const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, templateType);
|
||||||
|
if (!latest) {
|
||||||
|
return res.status(404).json({ error: 'No active template found for user type' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch template HTML from S3
|
||||||
|
const s3 = new S3Client({
|
||||||
|
region: process.env.EXOSCALE_REGION,
|
||||||
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
||||||
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: latest.storageKey });
|
||||||
|
const fileObj = await s3.send(command);
|
||||||
|
if (!fileObj.Body) {
|
||||||
|
return res.status(404).json({ error: 'Template file not available' });
|
||||||
|
}
|
||||||
|
let html = await streamToString(fileObj.Body, latest.id);
|
||||||
|
|
||||||
|
// Build variable map from DB for target user
|
||||||
|
const vars = {};
|
||||||
|
// Always include email and currentDate
|
||||||
|
vars.email = userRow.email || '';
|
||||||
|
const now = new Date();
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
vars.currentDate = `${pad(now.getDate())}.${pad(now.getMonth() + 1)}.${now.getFullYear()} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
||||||
|
|
||||||
|
// Personal profile fields
|
||||||
|
if (userType === 'personal') {
|
||||||
|
try {
|
||||||
|
const [pRows] = await db.execute('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [targetUserId]);
|
||||||
|
const p = (pRows && pRows[0]) ? pRows[0] : null;
|
||||||
|
if (p) {
|
||||||
|
const first = p.first_name || '';
|
||||||
|
const last = p.last_name || '';
|
||||||
|
const fullName = `${first} ${last}`.trim();
|
||||||
|
vars.fullName = fullName;
|
||||||
|
vars.address = p.address || '';
|
||||||
|
vars.zip_code = p.zip_code || '';
|
||||||
|
vars.city = p.city || '';
|
||||||
|
vars.country = p.country || '';
|
||||||
|
vars.phone = p.phone || '';
|
||||||
|
const fullAddressParts = [];
|
||||||
|
if (vars.address) fullAddressParts.push(vars.address);
|
||||||
|
const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' ');
|
||||||
|
if (zipCity) fullAddressParts.push(zipCity);
|
||||||
|
vars.fullAddress = fullAddressParts.join(', ');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[previewLatestForUser] load personal_profiles failed', e && e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Company profile fields
|
||||||
|
if (userType === 'company') {
|
||||||
|
try {
|
||||||
|
const [cRows] = await db.execute('SELECT * FROM company_profiles WHERE user_id = ? LIMIT 1', [targetUserId]);
|
||||||
|
const c = (cRows && cRows[0]) ? cRows[0] : null;
|
||||||
|
if (c) {
|
||||||
|
vars.companyName = c.company_name || '';
|
||||||
|
vars.registrationNumber = c.registration_number || '';
|
||||||
|
vars.companyAddress = c.address || '';
|
||||||
|
// generic address keys used by some templates
|
||||||
|
vars.address = vars.companyAddress;
|
||||||
|
vars.zip_code = c.zip_code || '';
|
||||||
|
vars.city = c.city || '';
|
||||||
|
vars.country = c.country || '';
|
||||||
|
vars.contactPersonName = c.contact_person_name || '';
|
||||||
|
vars.contactPersonPhone = c.contact_person_phone || c.phone || '';
|
||||||
|
vars.companyEmail = c.email || c.company_email || c.contact_email || userRow.email || '';
|
||||||
|
vars.companyPhone = c.phone || c.contact_person_phone || '';
|
||||||
|
const addrParts = [];
|
||||||
|
if (vars.companyAddress) addrParts.push(vars.companyAddress);
|
||||||
|
const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' ');
|
||||||
|
if (zipCity) addrParts.push(zipCity);
|
||||||
|
vars.companyFullAddress = addrParts.join(', ');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[previewLatestForUser] load company_profiles failed', e && e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace placeholders with resolved variables (simple global replace)
|
||||||
|
if (vars && typeof vars === 'object') {
|
||||||
|
Object.entries(vars).forEach(([key, value]) => {
|
||||||
|
const before = (html.match(new RegExp(`{{\\s*${key}\\s*}}`, 'g')) || []).length;
|
||||||
|
if (before) {
|
||||||
|
html = html.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), String(value ?? ''));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply stamp/signature placeholders (harmless if placeholders are absent)
|
||||||
|
try { html = await applyCompanyStampPlaceholders(html, { ...req, user: { ...userRow, user_type: userType } }); } catch (e) { logger.warn('[previewLatestForUser] applyCompanyStampPlaceholders failed', e && e.message); }
|
||||||
|
try { html = await applyProfitPlanetSignature(html); } catch (e) { logger.warn('[previewLatestForUser] applyProfitPlanetSignature failed', e && e.message); }
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
return res.send(ensureHtmlDocument(html));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[previewLatestForUser] error', err && err.stack ? err.stack : err);
|
||||||
|
return res.status(500).json({ error: 'Failed to render preview' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// NEW: Authenticated user endpoint to preview their own latest active contract with DB-filled placeholders
|
||||||
|
// GET /api/contracts/preview/latest
|
||||||
|
exports.previewLatestForMe = async (req, res) => {
|
||||||
|
if (!req.user) return res.status(401).json({ error: 'Unauthorized' });
|
||||||
|
const targetUserId = req.user.id || req.user.userId;
|
||||||
|
const userType = req.user.user_type || req.user.userType;
|
||||||
|
if (!targetUserId || !userType) return res.status(400).json({ error: 'Invalid authenticated user' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the latest active template for this user type
|
||||||
|
const latest = await DocumentTemplateService.getLatestActiveForUserType(userType, 'contract');
|
||||||
|
if (!latest) {
|
||||||
|
return res.status(404).json({ error: 'No active template found for your user type' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch template HTML from storage
|
||||||
|
const s3 = new S3Client({
|
||||||
|
region: process.env.EXOSCALE_REGION,
|
||||||
|
endpoint: process.env.EXOSCALE_ENDPOINT,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.EXOSCALE_ACCESS_KEY,
|
||||||
|
secretAccessKey: process.env.EXOSCALE_SECRET_KEY
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const command = new GetObjectCommand({ Bucket: process.env.EXOSCALE_BUCKET, Key: latest.storageKey });
|
||||||
|
const fileObj = await s3.send(command);
|
||||||
|
if (!fileObj.Body) return res.status(404).json({ error: 'Template file not available' });
|
||||||
|
let html = await streamToString(fileObj.Body, latest.id);
|
||||||
|
|
||||||
|
// Build variables from the authenticated user's DB data
|
||||||
|
const vars = {};
|
||||||
|
// base fields
|
||||||
|
vars.email = req.user.email || '';
|
||||||
|
const d = new Date();
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
vars.currentDate = `${pad(d.getDate())}.${pad(d.getMonth() + 1)}.${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
|
|
||||||
|
if (userType === 'personal') {
|
||||||
|
try {
|
||||||
|
const [rows] = await db.execute('SELECT * FROM personal_profiles WHERE user_id = ? LIMIT 1', [targetUserId]);
|
||||||
|
const p = rows && rows[0] ? rows[0] : null;
|
||||||
|
if (p) {
|
||||||
|
const fullName = `${p.first_name || ''} ${p.last_name || ''}`.trim();
|
||||||
|
vars.fullName = fullName;
|
||||||
|
vars.address = p.address || '';
|
||||||
|
vars.zip_code = p.zip_code || '';
|
||||||
|
vars.city = p.city || '';
|
||||||
|
vars.country = p.country || '';
|
||||||
|
vars.phone = p.phone || '';
|
||||||
|
const parts = [];
|
||||||
|
if (vars.address) parts.push(vars.address);
|
||||||
|
const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' ');
|
||||||
|
if (zipCity) parts.push(zipCity);
|
||||||
|
vars.fullAddress = parts.join(', ');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[previewLatestForMe] personal profile lookup failed', e && e.message);
|
||||||
|
}
|
||||||
|
} else if (userType === 'company') {
|
||||||
|
try {
|
||||||
|
const [rows] = await db.execute('SELECT * FROM company_profiles WHERE user_id = ? LIMIT 1', [targetUserId]);
|
||||||
|
const c = rows && rows[0] ? rows[0] : null;
|
||||||
|
if (c) {
|
||||||
|
vars.companyName = c.company_name || '';
|
||||||
|
vars.registrationNumber = c.registration_number || '';
|
||||||
|
vars.companyAddress = c.address || '';
|
||||||
|
vars.address = vars.companyAddress;
|
||||||
|
vars.zip_code = c.zip_code || '';
|
||||||
|
vars.city = c.city || '';
|
||||||
|
vars.country = c.country || '';
|
||||||
|
vars.contactPersonName = c.contact_person_name || '';
|
||||||
|
vars.contactPersonPhone = c.contact_person_phone || c.phone || '';
|
||||||
|
vars.companyEmail = c.email || c.company_email || c.contact_email || req.user.email || '';
|
||||||
|
vars.companyPhone = c.phone || c.contact_person_phone || '';
|
||||||
|
const parts = [];
|
||||||
|
if (vars.companyAddress) parts.push(vars.companyAddress);
|
||||||
|
const zipCity = [vars.zip_code, vars.city].filter(Boolean).join(' ');
|
||||||
|
if (zipCity) parts.push(zipCity);
|
||||||
|
vars.companyFullAddress = parts.join(', ');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('[previewLatestForMe] company profile lookup failed', e && e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace placeholders
|
||||||
|
Object.entries(vars).forEach(([k, v]) => {
|
||||||
|
html = html.replace(new RegExp(`{{\\s*${k}\\s*}}`, 'g'), String(v ?? ''));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply company stamp and signature where applicable
|
||||||
|
try { html = await applyCompanyStampPlaceholders(html, req); } catch (e) {}
|
||||||
|
try { html = await applyProfitPlanetSignature(html); } catch (e) {}
|
||||||
|
|
||||||
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
return res.send(ensureHtmlDocument(html));
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[previewLatestForMe] error', err && err.stack ? err.stack : err);
|
||||||
|
return res.status(500).json({ error: 'Failed to render preview' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// NEW controller: download sanitized PDF (variables emptied)
|
// NEW controller: download sanitized PDF (variables emptied)
|
||||||
// Keeps stamp/signature placeholders so company stamps or profitplanetSignature can still be applied if available.
|
// Keeps stamp/signature placeholders so company stamps or profitplanetSignature can still be applied if available.
|
||||||
exports.downloadPdf = async (req, res) => {
|
exports.downloadPdf = async (req, res) => {
|
||||||
|
|||||||
@ -59,6 +59,8 @@ router.get('/admin/verification-pending-users', authMiddleware, requireAdmin, Ad
|
|||||||
router.get('/admin/unverified-users', authMiddleware, requireAdmin, AdminUserController.getUnverifiedUsers);
|
router.get('/admin/unverified-users', authMiddleware, requireAdmin, AdminUserController.getUnverifiedUsers);
|
||||||
router.get('/admin/user/:id/documents', authMiddleware, requireAdmin, UserDocumentController.getAllDocumentsForUser);
|
router.get('/admin/user/:id/documents', authMiddleware, requireAdmin, UserDocumentController.getAllDocumentsForUser);
|
||||||
router.get('/admin/server-status', authMiddleware, requireAdmin, ServerStatusController.getStatus);
|
router.get('/admin/server-status', authMiddleware, requireAdmin, ServerStatusController.getStatus);
|
||||||
|
// Contract preview for admin: latest active by user type
|
||||||
|
router.get('/admin/contracts/:id/preview', authMiddleware, requireAdmin, DocumentTemplateController.previewLatestForUser);
|
||||||
|
|
||||||
// permissions.js GETs
|
// permissions.js GETs
|
||||||
router.get('/permissions', authMiddleware, PermissionController.list);
|
router.get('/permissions', authMiddleware, PermissionController.list);
|
||||||
@ -86,6 +88,9 @@ router.get('/contracts/company', authMiddleware, (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// User: preview latest active contract (HTML) for authenticated user
|
||||||
|
router.get('/contracts/preview/latest', authMiddleware, DocumentTemplateController.previewLatestForMe);
|
||||||
|
|
||||||
// documentTemplates.js GETs
|
// documentTemplates.js GETs
|
||||||
router.get('/document-templates', authMiddleware, DocumentTemplateController.listTemplates);
|
router.get('/document-templates', authMiddleware, DocumentTemplateController.listTemplates);
|
||||||
router.get('/document-templates/:id', authMiddleware, DocumentTemplateController.getTemplate);
|
router.get('/document-templates/:id', authMiddleware, DocumentTemplateController.getTemplate);
|
||||||
|
|||||||
@ -128,6 +128,20 @@ class DocumentTemplateService {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convenience: return the most recent active template for a user type (by createdAt desc)
|
||||||
|
async getLatestActiveForUserType(userType, templateType = 'contract') {
|
||||||
|
logger.info('DocumentTemplateService.getLatestActiveForUserType:start', { userType, templateType });
|
||||||
|
try {
|
||||||
|
const list = await DocumentTemplateRepository.findActiveByUserType(userType, templateType);
|
||||||
|
const latest = Array.isArray(list) && list.length ? list[0] : null;
|
||||||
|
logger.info('DocumentTemplateService.getLatestActiveForUserType:result', { found: !!latest, id: latest?.id });
|
||||||
|
return latest;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('DocumentTemplateService.getLatestActiveForUserType:error', { error: error.message });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NEW: Retrieve Profit Planet signature (company stamp) as <img> tag
|
// NEW: Retrieve Profit Planet signature (company stamp) as <img> tag
|
||||||
// Fallback order:
|
// Fallback order:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user