148 lines
5.7 KiB
JavaScript
148 lines
5.7 KiB
JavaScript
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}`))
|