'use strict'; require('dotenv').config(); const path = require('path'); const express = require('express'); const helmet = require('helmet'); const session = require('express-session'); const SqliteStore = require('better-sqlite3-session-store')(session); const db = require('./db'); // applies schema + first-run seed const app = express(); const PORT = Number(process.env.PORT) || 3000; const PROD = process.env.NODE_ENV === 'production'; app.set('trust proxy', 1); // Security headers. CSP is relaxed for inline bootstrapping but blocks remote // scripts; all libraries are served from our own /vendor. app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], // Alpine.js compiles its x-/@/: expressions with new Function(), which // requires 'unsafe-eval'. All scripts are self-hosted/vendored (no // remote or user-injected scripts), so the added risk is minimal. scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", 'data:'], fontSrc: ["'self'", 'data:'], connectSrc: ["'self'"], }, }, }) ); app.use(express.json({ limit: '256kb' })); app.use( session({ store: new SqliteStore({ client: db, expired: { clear: true, intervalMs: 900000 } }), secret: process.env.SESSION_SECRET || 'dev-insecure-secret-change-me', resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: 'lax', secure: PROD, maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days }, }) ); // Never let the browser cache API responses. Without this, a GET like /api/me // can be served stale from cache (e.g. a 200 from a prior session) while other // calls hit the network and 401 on an expired session — confusing and buggy. app.use('/api', (req, res, next) => { res.set('Cache-Control', 'no-store'); next(); }); // API routes app.use('/api', require('./src/routes/auth.routes')); app.use('/api/users', require('./src/routes/users.routes')); app.use('/api/schemes', require('./src/routes/schemes.routes')); app.use('/api/chart', require('./src/routes/chart.routes')); app.use('/api/projects', require('./src/routes/projects.routes')); app.use('/api/kpis', require('./src/routes/kpis.routes')); app.use('/api/entries', require('./src/routes/entries.routes')); // Silence the browser's automatic /favicon.ico request (otherwise a 404 shows // up in the console as a scary-looking "Failed to load resource"). app.get('/favicon.ico', (req, res) => res.status(204).end()); // Static frontend. Force revalidation of HTML/JS so the browser never runs a // stale cached build after an update (a frequent source of "I fixed it but it // still breaks" confusion). Fonts/CSS may still be cached via ETag/Last-Modified. app.use( express.static(path.join(__dirname, 'public'), { setHeaders(res, filePath) { if (/\.(html|js)$/i.test(filePath)) res.set('Cache-Control', 'no-cache'); }, }) ); // JSON 404 for unmatched API routes app.use('/api', (req, res) => res.status(404).json({ error: 'Not found' })); // Centralized error handler // eslint-disable-next-line no-unused-vars app.use((err, req, res, next) => { console.error(err); res.status(500).json({ error: 'Internal server error' }); }); app.listen(PORT, () => { console.log(`Bowler Chart running at http://localhost:${PORT}`); });