import crypto from 'node:crypto' import express from 'express' import { chromium } from 'playwright-core' import { initDb, getPool } from './db.js' import { publicAuthConfig, requireAuth } from './auth.js' import { renderEmailHtml, renderReportHtml } from './templateEngine.js' const app = express() const port = Number(process.env.PORT || 3000) const tenantId = process.env.TEMPLATE_TENANT_ID || 'default' app.use(express.json({ limit: '2mb' })) app.get('/api/health/ready', async (_req, res) => { try { await getPool().query('SELECT 1') res.json({ ok: true }) } catch (error) { res.status(503).json({ ok: false, error: error.message }) } }) app.get('/api/config', (_req, res) => res.json(publicAuthConfig())) app.use('/api', requireAuth) function userId(req) { return req.user?.preferred_username || req.user?.sub || 'unknown' } function defaultDocument(type) { return { blocks: [ { id: crypto.randomUUID(), type: 'heading', props: { text: type === 'email' ? 'Hello {{customer.name}}' : 'Report — {{report.date}}' } }, { id: crypto.randomUUID(), type: 'text', props: { text: type === 'email' ? 'Add your message here.' : 'Add report context here.' } }, { id: crypto.randomUUID(), type: 'kpi', props: { label: 'Total', value: '{{summary.total}}' } } ] } } app.get('/api/templates', async (req, res) => { const { rows } = await getPool().query( 'SELECT id,name,type,status,published_version,updated_at FROM templates WHERE tenant_id=$1 ORDER BY updated_at DESC', [tenantId] ) res.json(rows) }) app.post('/api/templates', async (req, res) => { const name = String(req.body?.name || '').trim() const type = req.body?.type === 'email' ? 'email' : 'report' if (!name) return res.status(400).json({ error: 'name_required' }) const id = crypto.randomUUID() const document = defaultDocument(type) const sampleData = { customer: { name: 'Example Customer' }, report: { date: new Date().toISOString().slice(0, 10) }, summary: { total: 125 } } const { rows } = await getPool().query( `INSERT INTO templates (id,tenant_id,name,type,draft_document,sample_data,created_by,updated_by) VALUES ($1,$2,$3,$4,$5,$6,$7,$7) RETURNING *`, [id, tenantId, name, type, document, sampleData, userId(req)] ) res.status(201).json(rows[0]) }) app.get('/api/templates/:id', async (req, res) => { const { rows } = await getPool().query('SELECT * FROM templates WHERE id=$1 AND tenant_id=$2', [req.params.id, tenantId]) if (!rows[0]) return res.status(404).json({ error: 'not_found' }) res.json(rows[0]) }) app.put('/api/templates/:id/draft', async (req, res) => { const document = req.body?.document const sampleData = req.body?.sampleData ?? {} if (!document || !Array.isArray(document.blocks)) return res.status(400).json({ error: 'invalid_document' }) const { rows } = await getPool().query( `UPDATE templates SET draft_document=$1,sample_data=$2,updated_by=$3,updated_at=now() WHERE id=$4 AND tenant_id=$5 RETURNING *`, [document, sampleData, userId(req), req.params.id, tenantId] ) if (!rows[0]) return res.status(404).json({ error: 'not_found' }) res.json(rows[0]) }) app.post('/api/templates/:id/publish', async (req, res) => { const db = await getPool().connect() try { await db.query('BEGIN') const current = await db.query('SELECT * FROM templates WHERE id=$1 AND tenant_id=$2 FOR UPDATE', [req.params.id, tenantId]) const template = current.rows[0] if (!template) { await db.query('ROLLBACK') return res.status(404).json({ error: 'not_found' }) } const nextVersion = (template.published_version || 0) + 1 await db.query( `INSERT INTO template_versions (id,template_id,version,document,sample_data,created_by) VALUES ($1,$2,$3,$4,$5,$6)`, [crypto.randomUUID(), template.id, nextVersion, template.draft_document, template.sample_data, userId(req)] ) const updated = await db.query( `UPDATE templates SET status='published',published_version=$1,updated_at=now(),updated_by=$2 WHERE id=$3 RETURNING *`, [nextVersion, userId(req), template.id] ) await db.query('COMMIT') res.json(updated.rows[0]) } catch (error) { await db.query('ROLLBACK') throw error } finally { db.release() } }) app.post('/api/templates/:id/render', async (req, res) => { const { rows } = await getPool().query('SELECT * FROM templates WHERE id=$1 AND tenant_id=$2', [req.params.id, tenantId]) const template = rows[0] if (!template) return res.status(404).json({ error: 'not_found' }) const data = req.body?.data ?? template.sample_data ?? {} const format = req.body?.format || (template.type === 'email' ? 'email' : 'html') const document = template.draft_document if (format === 'email') return res.json({ html: renderEmailHtml(document, data) }) const html = renderReportHtml(document, data) if (format === 'html') return res.json({ html }) if (format !== 'pdf') return res.status(400).json({ error: 'unsupported_format' }) const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium', args: ['--no-sandbox', '--disable-dev-shm-usage'] }) try { const page = await browser.newPage() await page.setContent(html, { waitUntil: 'networkidle' }) const pdf = await page.pdf({ format: 'A4', printBackground: true }) res.type('application/pdf').send(pdf) } finally { await browser.close() } }) app.use(express.static('dist')) app.get(/^(?!\/api).*/, (_req, res) => res.sendFile('index.html', { root: 'dist' })) app.use((error, _req, res, _next) => { console.error(error) res.status(500).json({ error: 'internal_error' }) }) await initDb() app.listen(port, '0.0.0.0', () => console.log(`Template Studio listening on :${port}`))