import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import cookieParser from 'cookie-parser'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { existsSync } from 'node:fs'; import { env } from './config/env.js'; import { logger } from './lib/logger.js'; import chatRoutes from './routes/chat.js'; import tripsRoutes from './routes/trips.js'; import teslaRoutes from './routes/tesla.js'; import ownerRoutes from './routes/owner.js'; import { warnIfMisconfigured as warnOwnerAuth } from './lib/ownerAuth.js'; import { createOptionalAuth } from './lib/auth.js'; const app = express(); // We sit behind one Nginx hop (Dokku's per-app vhost). Trust exactly one // proxy so req.ip reflects the real client and the rate limiters below // can't be bypassed via a spoofed X-Forwarded-For header. app.set('trust proxy', 1); app.use(helmet({ contentSecurityPolicy: false })); app.use(cors({ origin: env.appUrl, credentials: true })); app.use(express.json({ limit: '2mb' })); app.use(cookieParser()); app.use((req, _res, next) => { if (req.url !== '/health') logger.info({ method: req.method, url: req.url }, 'request'); next(); }); app.get('/health', (_req, res) => { res.json({ status: 'ok', service: 'tesla-roadtrip', time: new Date().toISOString() }); }); const auth = createOptionalAuth(); if (auth) { app.use(auth.middleware()); app.use(auth.routes()); logger.info('Auth middleware mounted (AUTH_SECRET present)'); } else { logger.info('Auth disabled — set AUTH_SECRET to enable user accounts'); } // Owner auth + Tesla integration. Tesla routes are owner-gated except the // public .well-known partner-key path. Owner routes handle login/logout. warnOwnerAuth(); app.use(ownerRoutes); app.use(teslaRoutes); app.use('/api', chatRoutes); app.use('/api/trips', tripsRoutes); // ─── Static client (production only) ───────────────────────────────────────── // In dev, Vite serves the client on :5173. In production (Dokku), the built // client lands in client/dist via `npm run build` and we serve it from here. const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const clientDist = path.resolve(__dirname, '../../client/dist'); if (existsSync(clientDist)) { app.use(express.static(clientDist, { index: false, maxAge: '1h' })); app.get(/.*/, (req, res, next) => { // Don't shadow API or well-known paths. if (req.path.startsWith('/api') || req.path.startsWith('/.well-known')) return next(); res.sendFile(path.join(clientDist, 'index.html')); }); logger.info({ clientDist }, 'Serving built client'); } else { logger.info('No client/dist found — relying on Vite dev server'); } app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => { logger.error({ err }, 'Unhandled error'); res.status(500).json({ error: 'Internal server error' }); }); app.listen(env.port, () => { logger.info(`Tesla Roadtrip server running on port ${env.port}`); });