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:
2026-07-18 23:15:29 +07:00
commit fe89bb2b1c
54 changed files with 8565 additions and 0 deletions
+74
View File
@@ -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;