const express = require('express'); const path = require('node:path'); const multer = require('multer'); const { createContractPdf, listTemplates, getTemplatePreviewPdf, saveTemplate, deleteTemplate, } = require('./src/contractService'); const app = express(); const port = Number(process.env.PORT || 3001); const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin'; if (!process.env.ADMIN_PASSWORD) { console.warn('[warn] ADMIN_PASSWORD 未設定,使用預設密碼 "admin"'); } app.use(express.json({ limit: '1mb' })); app.use(express.static(path.join(__dirname, 'public'))); function adminAuth(req, res, next) { const auth = req.headers.authorization || ''; if (!auth.startsWith('Basic ')) { return res.status(401).json({ error: '需要管理員驗證。' }); } const decoded = Buffer.from(auth.slice(6), 'base64').toString('utf-8'); const password = decoded.slice(decoded.indexOf(':') + 1); if (password !== ADMIN_PASSWORD) { return res.status(401).json({ error: '密碼錯誤。' }); } next(); } const upload = multer({ storage: multer.memoryStorage(), fileFilter: (_req, file, cb) => { const name = Buffer.from(file.originalname, 'latin1').toString('utf8'); file.originalname = name; const ext = path.extname(name).toLowerCase(); if (ext === '.doc' || ext === '.docx') return cb(null, true); const err = new Error('僅接受 .doc / .docx 檔案。'); err.exposeToClient = true; cb(err); }, limits: { fileSize: 10 * 1024 * 1024 }, }); // ── Public routes ────────────────────────────────────────── app.get('/api/health', (_req, res) => { res.json({ ok: true }); }); app.get('/api/templates', async (_req, res, next) => { try { res.json({ templates: await listTemplates() }); } catch (error) { next(error); } }); app.get('/api/templates/:name/preview', async (req, res) => { try { const pdfBuffer = await getTemplatePreviewPdf(req.params.name, req.query); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Cache-Control', 'no-store'); res.send(pdfBuffer); } catch (error) { console.error(error); const msg = error.exposeToClient ? error.message : '預覽失敗'; res.status(error.exposeToClient ? 400 : 500) .send(`${msg}`); } }); app.post('/api/contracts/pdf', async (req, res) => { try { const result = await createContractPdf({ template: req.body.template, monthlyRent: req.body.monthlyRent, paymentDay: req.body.paymentDay, deposit: req.body.deposit, leaseStart: req.body.leaseStart, leaseEnd: req.body.leaseEnd, }); res.setHeader('Content-Type', 'application/pdf'); res.setHeader( 'Content-Disposition', `attachment; filename="rental-contract.pdf"; filename*=UTF-8''${encodeURIComponent(result.fileName)}`, ); res.setHeader('Cache-Control', 'no-store'); res.send(result.buffer); } catch (error) { console.error(error); const status = error.exposeToClient ? 400 : 500; res.status(status).json({ error: error.exposeToClient ? error.message : 'PDF 產生失敗,請確認 LibreOffice 已安裝且範本格式正確。', }); } }); // ── Admin routes ─────────────────────────────────────────── app.get('/api/admin/check', adminAuth, (_req, res) => { res.json({ ok: true }); }); app.post('/api/admin/templates', adminAuth, (req, res) => { upload.single('template')(req, res, async (err) => { if (err) { const status = err.exposeToClient || err instanceof multer.MulterError ? 400 : 500; return res.status(status).json({ error: err.message }); } try { if (!req.file) return res.status(400).json({ error: '請選擇檔案。' }); const name = await saveTemplate(req.file.originalname, req.file.buffer); res.json({ ok: true, name }); } catch (error) { console.error(error); res.status(error.exposeToClient ? 400 : 500).json({ error: error.exposeToClient ? error.message : '上傳失敗。', }); } }); }); app.delete('/api/admin/templates/:name', adminAuth, async (req, res, next) => { try { await deleteTemplate(req.params.name); res.json({ ok: true }); } catch (error) { next(error); } }); // ── Error handler ────────────────────────────────────────── app.use((error, _req, res, _next) => { console.error(error); const status = error.exposeToClient ? 400 : 500; res.status(status).json({ error: error.exposeToClient ? error.message : '伺服器發生錯誤。', }); }); app.listen(port, () => { console.log(`Rental contract PDF app is running at http://localhost:${port}`); });