Files
rental-contract-pdf/server.js

154 lines
4.9 KiB
JavaScript
Raw Normal View History

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(`<!DOCTYPE html><html><body style="padding:20px;color:#666;font-family:sans-serif">${msg}</body></html>`);
}
});
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}`);
});