156 lines
5.8 KiB
JavaScript

import crypto from 'node:crypto'
import express from 'express'
import { chromium } from 'playwright-core'
import { initDb, getPool } from './db.js'
import { registerAuthRoutes, requireAuth } from './auth.js'
import { renderEmailHtml, renderReportHtml } from './templateEngine.js'
const app = express()
const port = Number(process.env.PORT || 3000)
const tenantId = process.env.TENANT_ID || process.env.TEMPLATE_TENANT_ID || 'default'
app.set('trust proxy', 1)
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 })
}
})
registerAuthRoutes(app)
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 },
items: [{ name: 'Example row', value: 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|\/auth).*/, (_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}`))