const express = require('express'); const cookieParser = require('cookie-parser'); const path = require('path'); const { getDb } = require('./db/database'); const { requireAuth, requireUser, requireAdmin } = require('./middleware/requireAuth'); const { recordError } = require('./services/statusRuntime'); const { securityHeaders } = require('./middleware/securityHeaders'); const { importLimiter, exportLimiter, adminActionLimiter, oidcLimiter } = require('./middleware/rateLimiter'); const app = express(); const PORT = process.env.PORT || 3000; const DIST = path.join(__dirname, 'dist'); // ── Security headers (applied to every response) ────────────────────────────── app.use(securityHeaders); // ── CORS — disabled unless CORS_ORIGIN is explicitly configured ─────────────── // The default same-origin deployment (Express serves both API and UI) needs no // CORS headers. Set CORS_ORIGIN=https://your-frontend.example.com only when // the frontend is hosted on a separate origin. if (process.env.CORS_ORIGIN) { const cors = require('cors'); const allowed = process.env.CORS_ORIGIN.split(',').map(s => s.trim()).filter(Boolean); app.use(cors({ credentials: true, origin: allowed })); } app.use(express.json()); app.use(cookieParser()); // ── API ─────────────────────────────────────────────────────────────────────── // Auth — login and password-change rate limits are applied inside the route file app.use('/api/auth', require('./routes/auth')); // OIDC — rate-limited; returns 501 gracefully when OIDC is not configured app.use('/api/auth/oidc', oidcLimiter, require('./routes/authOidc')); // Admin — all routes already require auth+admin; mutation-heavy routes get // an additional per-IP rate limit applied to the whole admin namespace app.use('/api/admin', requireAuth, requireAdmin, adminActionLimiter, require('./routes/admin')); app.use('/api/tracker', requireAuth, requireUser, require('./routes/tracker')); app.use('/api/bills', requireAuth, requireUser, require('./routes/bills')); app.use('/api/payments', requireAuth, requireUser, require('./routes/payments')); app.use('/api/categories', requireAuth, requireUser, require('./routes/categories')); app.use('/api/settings', requireAuth, requireUser, require('./routes/settings')); app.use('/api/notifications', requireAuth, require('./routes/notifications')); app.use('/api/status', requireAuth, require('./routes/status')); app.use('/api/version', require('./routes/version')); // public // Profile — password-change rate limit applied inside the route file app.use('/api/profile', requireAuth, requireUser, require('./routes/profile')); // Export / Import — per-IP rate limited to deter abuse and resource exhaustion app.use('/api/export', requireAuth, requireUser, exportLimiter, require('./routes/export')); app.use('/api/import', requireAuth, requireUser, importLimiter, require('./routes/import')); // ── Legacy UI ("Remember When" mode) ───────────────────────────────────────── app.use('/legacy', express.static(path.join(__dirname, 'legacy'))); // ── Modern UI (Vite build) ──────────────────────────────────────────────────── app.get('/login.html', (req, res) => res.redirect(302, '/login')); app.use(express.static(DIST)); app.get('*', (req, res) => res.sendFile(path.join(DIST, 'index.html'))); // ── Global error handler ────────────────────────────────────────────────────── // Never expose stack traces, internal paths, or raw error objects in responses. app.use((err, req, res, next) => { recordError('Express', err); console.error('[error]', err.message || String(err)); if (req.path?.startsWith('/api/import/')) { const isBodyError = err.type === 'entity.too.large' || err instanceof SyntaxError; return res.status(err.status || (isBodyError ? 400 : 500)).json({ error: 'Import request failed', message: isBodyError ? 'The import request could not be read. Please retry with a smaller or valid file.' : 'Unexpected import server error. Please try again.', code: isBodyError ? 'IMPORT_REQUEST_ERROR' : 'IMPORT_SERVER_ERROR', }); } res.status(err.status || 500).json({ error: err.status ? err.message : 'Internal server error', }); }); // ── Bootstrap ───────────────────────────────────────────────────────────────── async function main() { const db = getDb(); const userCount = db.prepare('SELECT COUNT(*) AS count FROM users').get().count; if (userCount === 0) await require('./setup/firstRun').run(db); require('./workers/dailyWorker').start(); require('./services/backupScheduler').start(); app.listen(PORT, () => console.log(`Bill Tracker running at http://localhost:${PORT}`)); } main().catch(err => { console.error('Startup failed:', err); process.exit(1); });