Files

119 lines
4.9 KiB
JavaScript
Raw Permalink Normal View History

2026-09-13 20:09:20 +01:00
'use strict';
require('dotenv').config();
const path = require('path');
const express = require('express');
const session = require('express-session');
const SqliteStore = require('better-sqlite3-session-store')(session);
const { db, init, migrate } = require('./db');
const { seedDefaults } = require('./lib/templates');
const { requireAuth, requireAdmin } = require('./middleware/auth');
init();
migrate();
seedDefaults();
const app = express();
app.disable('x-powered-by');
app.set('trust proxy', 1);
app.use(express.json({ limit: '2mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(session({
store: new SqliteStore({
client: db,
expired: { clear: true, intervalMs: 15 * 60 * 1000 },
}),
secret: process.env.SESSION_SECRET || 'change-me-in-production',
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
sameSite: 'lax',
secure: String(process.env.COOKIE_SECURE || 'false') === 'true',
maxAge: 1000 * 60 * 60 * 8, // 8h
},
}));
// ---- First-boot setup gate -------------------------------------------
// While the instance has no users, every page redirects to the setup wizard
// so the first administrator can be created through the browser.
const setup = require('./routes/setup');
app.use('/api/setup', setup.router);
app.use((req, res, next) => {
const url = req.originalUrl || req.url;
// Always allow the setup API, the wizard page itself, and static assets
// (css/js) — otherwise the wizard could not render.
if (url.startsWith('/api/setup') || url.startsWith('/setup.html') ||
url.startsWith('/css/') || url.startsWith('/js/') || url.startsWith('/assets/')) return next();
if (setup.needsSetup()) {
if (url.startsWith('/api/')) {
return res.status(503).json({ error: 'Setup required. Open the site in a browser to create the administrator account.' });
}
return res.redirect('/setup.html');
}
// Setup is done — the wizard must never be reachable again.
next();
});
// ---- API routes -------------------------------------------------------
app.use('/api/auth', require('./routes/auth'));
app.use('/api/manage', require('./routes/manage')); // admin only (enforced inside)
app.use('/api/datacreate', require('./routes/datacreate')); // admin only (enforced inside)
app.use('/api/users', require('./routes/users')); // admin only (enforced inside)
app.use('/api/versions', require('./routes/versions')); // read: all users, write: admin
app.use('/api/mail', require('./routes/mail')); // admin only (enforced inside)
app.use('/api/legislation', require('./routes/legislation')); // read: all users, write: admin
app.use('/api/storage', require('./routes/storage')); // admin only (enforced inside)
app.use('/api/data', require('./routes/data'));
app.use('/api/logs', require('./routes/logs')); // admin only (enforced inside)
app.use('/settings', require('./routes/settings')); // brand images served here
// ---- Page gating ------------------------------------------------------
// Serve the login page and static assets openly; gate the app pages.
const PUB = path.join(__dirname, 'public');
app.get('/', (req, res) => res.redirect(req.session.user ? '/view-data.html' : '/login.html'));
// The setup wizard is only reachable while no users exist.
app.get('/setup.html', (req, res) => {
if (!setup.needsSetup()) return res.redirect('/login.html');
res.sendFile(path.join(PUB, 'setup.html'));
});
// Protect the admin-only HTML pages at the route level too (defence in depth)
app.get('/data-management.html', requireAuth, requireAdmin, (req, res) =>
res.sendFile(path.join(PUB, 'data-management.html')));
app.get('/storage.html', requireAuth, requireAdmin, (req, res) =>
res.sendFile(path.join(PUB, 'storage.html')));
app.get('/logs.html', requireAuth, requireAdmin, (req, res) =>
res.sendFile(path.join(PUB, 'logs.html')));
// Signed-in pages
app.get('/view-data.html', requireAuth, (req, res) =>
res.sendFile(path.join(PUB, 'view-data.html')));
app.get('/calendar-view.html', requireAuth, (req, res) =>
res.sendFile(path.join(PUB, 'calendar-view.html')));
app.get('/account.html', requireAuth, (req, res) =>
res.sendFile(path.join(PUB, 'account.html')));
app.get('/version-control.html', requireAuth, requireAdmin, (req, res) =>
res.sendFile(path.join(PUB, 'version-control.html')));
app.get('/legislation.html', requireAuth, (req, res) =>
res.sendFile(path.join(PUB, 'legislation.html')));
app.use(express.static(PUB)); // login.html, css, js, assets
// ---- 404 / errors -----------------------------------------------------
app.use((req, res) => res.status(404).send('Not found.'));
app.use((err, req, res, next) => {
console.error(err);
if (res.headersSent) return next(err);
res.status(500).json({ error: err.message || 'Server error' });
});
const PORT = Number(process.env.PORT || 3000);
app.listen(PORT, () => {
console.log(`Infosec v2.3 running on http://localhost:${PORT}`);
});