import 'dotenv/config'; import express from 'express'; import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; import { log } from './config/logger.js'; import requestResponseLogger from './middleware/logging.js'; import { initDatabase } from './config/database.js'; import { initMailer } from './config/mailer.js'; import authRoutes from './routes/auth.js'; import roomRoutes from './routes/rooms.js'; import recordingRoutes from './routes/recordings.js'; import adminRoutes from './routes/admin.js'; import brandingRoutes from './routes/branding.js'; import federationRoutes, { wellKnownHandler } from './routes/federation.js'; import calendarRoutes from './routes/calendar.js'; import caldavRoutes from './routes/caldav.js'; import notificationRoutes from './routes/notifications.js'; import oauthRoutes from './routes/oauth.js'; import analyticsRoutes from './routes/analytics.js'; import { startFederationSync } from './jobs/federationSync.js'; import { startCalendarReminders } from './jobs/calendarReminders.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const PORT = process.env.PORT || 3001; // Trust proxy - configurable via TRUST_PROXY env var (default: 1 = one local reverse proxy) // Use a number to trust that many hops, or a string like 'loopback' / an IP/CIDR. const rawTrustProxy = process.env.TRUST_PROXY ?? 'loopback'; const trustProxy = /^\d+$/.test(rawTrustProxy) ? parseInt(rawTrustProxy, 10) : rawTrustProxy; app.set('trust proxy', trustProxy); // ── Security headers ─────────────────────────────────────────────────────── app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); if (process.env.NODE_ENV === 'production') { res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); } next(); }); // Middleware // M10: restrict CORS in production; deny cross-origin by default const corsOptions = process.env.APP_URL ? { origin: process.env.APP_URL, credentials: true } : { origin: false }; app.use(cors(corsOptions)); app.use(express.json({ limit: '100kb' })); // Request/Response logging (filters sensitive fields) app.use(requestResponseLogger); // Initialize database & start server async function start() { await initDatabase(); initMailer(); // Serve uploaded files (branding only — avatars served via /api/auth/avatar/:filename, presentations require auth) const uploadsPath = path.join(__dirname, '..', 'uploads'); app.use('/uploads/branding', express.static(path.join(uploadsPath, 'branding'))); // API Routes app.use('/api/auth', authRoutes); app.use('/api/rooms', roomRoutes); app.use('/api/recordings', recordingRoutes); app.use('/api/admin', adminRoutes); app.use('/api/branding', brandingRoutes); app.use('/api/federation', federationRoutes); app.use('/api/calendar', calendarRoutes); app.use('/api/notifications', notificationRoutes); app.use('/api/oauth', oauthRoutes); app.use('/api/analytics', analyticsRoutes); // CalDAV — mounted outside /api so calendar clients use a clean path app.use('/caldav', caldavRoutes); // Mount calendar federation receive also under /api/federation for remote instances app.use('/api/federation', calendarRoutes); app.get('/.well-known/redlight', wellKnownHandler); // ── CalDAV service discovery (RFC 6764) ────────────────────────────────── // Clients probe /.well-known/caldav then PROPFIND / before they know the // real CalDAV mount point. Redirect them to /caldav/ for all HTTP methods. app.all('/.well-known/caldav', (req, res) => { res.redirect(301, '/caldav/'); }); // Some clients (e.g. Thunderbird) send PROPFIND / directly at the server root. // Express doesn't register non-standard methods, so intercept via middleware. app.use('/', (req, res, next) => { if (req.method === 'PROPFIND' && req.path === '/') { return res.redirect(301, '/caldav/'); } next(); }); // Serve static files in production if (process.env.NODE_ENV === 'production') { app.use(express.static(path.join(__dirname, '..', 'dist'))); app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '..', 'dist', 'index.html')); }); } app.listen(PORT, () => { log.server.info(`Redlight server running on http://localhost:${PORT}`); }); // Start periodic federation sync job (checks remote room settings every 60s) startFederationSync(); // Start calendar reminder job (sends in-app + browser notifications before events) startCalendarReminders(); } start().catch(err => { log.server.error(`Failed to start server: ${err.message}`); process.exit(1); });