101 lines
3.4 KiB
JavaScript
101 lines
3.4 KiB
JavaScript
'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}`);
|
|
});
|