Multi-user trip planning web app in a single Docker container. Mullvad-style token accounts, trip sharing via join codes, day-by-day calendar with typed entries (activity, hotel, travel, flight, rental car, immigration, note), multi-leg flight segments with bundled IATA airport dataset, Leaflet/OSM map with per-leg great-circle km (air vs ground), rough km-driven vs rental included-km comparison, cost splitting with settle-up suggestions, flip-clock departure countdown. Node 20 + Express + SQLite (WAL, additive migrations), vanilla JS SPA, 44 API tests.
75 lines
2.4 KiB
JavaScript
75 lines
2.4 KiB
JavaScript
import express from 'express';
|
|
import cookieSession from 'cookie-session';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { openDb } from './db.js';
|
|
import { requireAuth } from './auth.js';
|
|
import authRoutes from './routes/auth.js';
|
|
import tripsRoutes from './routes/trips.js';
|
|
import entriesRoutes from './routes/entries.js';
|
|
import geocodeRoutes from './routes/geocode.js';
|
|
import airportsRoutes from './routes/airports.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const PUBLIC_DIR = path.join(__dirname, '..', '..', 'public');
|
|
|
|
// Build the Express app. Accepts either an options object
|
|
// { dbPath, sessionSecret } or a bare dbPath string (per API.md).
|
|
export function createApp(options = {}) {
|
|
const opts = typeof options === 'string' ? { dbPath: options } : options;
|
|
const dbPath = opts.dbPath || ':memory:';
|
|
const sessionSecret = opts.sessionSecret || 'dev-secret';
|
|
|
|
const db = openDb(dbPath);
|
|
const app = express();
|
|
app.disable('x-powered-by');
|
|
|
|
app.use(express.json());
|
|
app.use(
|
|
cookieSession({
|
|
name: 'trip_session',
|
|
secret: sessionSecret,
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
maxAge: 30 * 24 * 60 * 60 * 1000,
|
|
})
|
|
);
|
|
|
|
// no-cache (not no-store): browsers must revalidate, ETags turn unchanged
|
|
// files into 304s. Without this, upgraded deployments keep serving a stale
|
|
// cached frontend against the new API.
|
|
app.use(
|
|
express.static(PUBLIC_DIR, {
|
|
etag: true,
|
|
lastModified: true,
|
|
setHeaders: (res) => res.setHeader('Cache-Control', 'no-cache'),
|
|
})
|
|
);
|
|
|
|
app.use('/api/auth', authRoutes(db));
|
|
app.use('/api/trips', requireAuth, tripsRoutes(db));
|
|
app.use('/api', requireAuth, entriesRoutes(db)); // /trips/:id/entries + /entries/:id
|
|
app.use('/api/geocode', requireAuth, geocodeRoutes(db));
|
|
app.use('/api/airports', requireAuth, airportsRoutes());
|
|
|
|
// JSON 404 for any unmatched /api route.
|
|
app.use('/api', (req, res) => {
|
|
res.status(404).json({ error: 'not found' });
|
|
});
|
|
|
|
// JSON error handler (malformed body, unexpected failures).
|
|
// eslint-disable-next-line no-unused-vars
|
|
app.use((err, req, res, next) => {
|
|
if (err && err.type === 'entity.parse.failed') {
|
|
return res.status(400).json({ error: 'invalid JSON body' });
|
|
}
|
|
console.error(err);
|
|
res.status(500).json({ error: 'internal server error' });
|
|
});
|
|
|
|
app.locals.db = db;
|
|
return app;
|
|
}
|
|
|
|
export default createApp;
|