// ───────────────────────────────────────────────────────────── // server.js — entry point. // // Binds to 127.0.0.1 only. Nginx reverse-proxies to this port. // Never expose Node directly to the public internet. // ───────────────────────────────────────────────────────────── import 'dotenv/config'; import express from 'express'; import cookieParser from 'cookie-parser'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import authRouter from './src/auth.js'; import { requireAuth, requireAdmin } from './src/middleware.js'; import { q } from './src/db.js'; // also side-effect: opens DB + runs schema import { MOBILE_UA_RE } from './src/ua.js'; import { recordEvent } from './src/events.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const app = express(); // ─── Trust Nginx as the single upstream proxy ──────────────── // This makes req.ip reflect X-Forwarded-For (the real client IP) // instead of 127.0.0.1. Only "loopback" is trusted, so spoofed // XFF headers from the public internet are ignored. app.set('trust proxy', 'loopback'); // ─── Body parsers ──────────────────────────────────────────── app.use(express.json({ limit: '4kb' })); app.use(cookieParser()); // ─── Security headers on every response ────────────────────── app.use((req, res, next) => { res.set({ 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', 'Referrer-Policy': 'strict-origin-when-cross-origin', 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()', // Tight CSP — only our own origin, and inline