Initial release: collaborative trip planner
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.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user