/*********************************************************************************************************************************************************************** * File Name: server.js * Type of Program: Application Entry Point * Description: Bootstraps the Express application. * Registers global middleware (CORS, rate-limit, cookie-parser, * session, Passport) and mounts all route groups. * Syncs Sequelize models to the database on startup. * * Author: rgrgogu * Modified by: Kenneth Obsequio (@lash0000) * Date Created: Oct. 6, 2025 * Date Modified: Jun. 17, 2026 *********************************************************************************************************************************************************************** * HOW TO RUN: * 1. cp .env-development .env (or .env-production for self-hosted/Docker — fill in your values) * 2. npm install * 3. npm run dev (development) * npm start (production) ***********************************************************************************************************************************************************************/ require('dotenv').config(); const express = require('express'); const cors = require('cors'); const cookieParser = require('cookie-parser'); const session = require('express-session'); const sequelize = require('./config/db.config'); // ── Middleware ────────────────────────────────────────────────────────────────── const { globalLimiter } = require('./middleware/rateLimiter.middleware'); const { csrfErrorHandler } = require('./middleware/csrf.middleware'); const originGuard = require('./middleware/originGuard.middleware'); // ── Routes ───────────────────────────────────────────────────────────────────── const healthRoutes = require('./routes/health.routes'); const authRoutes = require('./routes/auth.routes'); const publicRoutes = require('./routes/public/public.routes'); const clientRoutes = require('./routes/client/client.routes'); const staffRoutes = require('./routes/staff/staff.routes'); const adminRoutes = require('./routes/admin/admin.routes'); // ── Models (ensure associations are loaded) ──────────────────────────────────── require('./models/users/users.mdl'); require('./models/users/user_sessions.mdl'); require('./models/users/user_groups.mdl'); require('./models/notifications/admin_notification.mdl'); require('./models/notifications/user_notification.mdl'); require('./models/notifications/notification_broadcast.mdl'); // ── Cron jobs ────────────────────────────────────────────────────────────────── const { startAdminCronJobs } = require('./cron/admin.cron'); const { startClientCronJobs } = require('./cron/client.cron'); const app = express(); const PORT = process.env.PORT || 3000; // ────────────────────────────────────────────────────────────────────────────── // Global Middleware Stack // ────────────────────────────────────────────────────────────────────────────── app.set('trust proxy', 1); // Required for rate-limiter behind proxies/load balancers const allowedOrigins = (process.env.ALLOWED_ORIGINS || process.env.APP_URL || '*').split(','); app.use(cors({ origin: (origin, callback) => { if (origin && allowedOrigins.includes(origin)) return callback(null, true); callback(null, false); }, credentials: true, preflightContinue: false, // cors() handles OPTIONS and stops — don't pass to next() methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: [ 'Content-Type', 'Authorization', 'Accept', 'Origin', 'X-Requested-With', 'csrf-token', 'x-csrf-token', 'CSRF-Token', 'X-CSRF-Token', ], exposedHeaders: ['Content-Disposition'], optionsSuccessStatus: 204, maxAge: 86400, })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Secret enables req.signedCookies, used by the OIDC state cookie (_oauth). app.use(cookieParser(process.env.SESSION_SECRET)); // Session — used only by csurf; JWT handles auth state. // CACHE_DRIVER=redis → persist sessions in PostgreSQL (survives restarts). // CACHE_DRIVER=memory → in-process MemoryStore (fine for local dev). const sessionStore = process.env.CACHE_DRIVER !== 'memory' ? new (require('connect-pg-simple')(session))({ conString: `postgresql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}?sslmode=${process.env.DB_SSL === 'false' ? 'disable' : 'require'}`, tableName: 'express_sessions', createTableIfMissing: true, }) : undefined; // undefined → express-session uses MemoryStore app.use(session({ ...(sessionStore && { store: sessionStore }), secret: process.env.SESSION_SECRET || 'change-me', resave: false, saveUninitialized: false, // sameSite:'none' (not 'strict') in production — frontend (Vercel) and this // API (Render) are cross-site, so 'strict' drops the cookie on every // fetch/XHR call. 'none' requires secure:true, already set above. cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'strict', }, })); // Global rate limiter (1000 req / 15 min) app.use(globalLimiter); // ────────────────────────────────────────────────────────────────────────────── // Routes // ────────────────────────────────────────────────────────────────────────────── // Health — no originGuard: monitoring tools (Uptime Robot, Pingdom, k8s probes) have no Origin app.use('/api/health', healthRoutes); app.use('/api/auth', originGuard, authRoutes); app.use('/api/public', originGuard, publicRoutes); app.use('/api/client', originGuard, clientRoutes); app.use('/api/staff', originGuard, staffRoutes); app.use('/api/admin', originGuard, adminRoutes); // 404 handler app.use((req, res) => { res.status(404).json({ status: 'error', message: 'Route not found.' }); }); // CSRF error handler (must be after routes) app.use(csrfErrorHandler); // Global error handler app.use((err, req, res, next) => { console.error('[UNHANDLED ERROR]', err); res.status(500).json({ status: 'error', message: 'Internal server error.' }); }); // ────────────────────────────────────────────────────────────────────────────── // Database Sync + Listen // ────────────────────────────────────────────────────────────────────────────── function printCronTable(jobs) { if (!jobs.length) { console.log('\n[ CRON ] No jobs scheduled.\n'); return; } const H = { name: 'Name', scope: 'Scope', schedule: 'Schedule' }; const w = { name: Math.max(H.name.length, ...jobs.map(j => j.name.length)), scope: Math.max(H.scope.length, ...jobs.map(j => j.scope.length)), schedule: Math.max(H.schedule.length, ...jobs.map(j => j.schedule.length)), }; const cell = (v, width) => ` ${v.padEnd(width)} `; const row = (n, s, c) => `│${cell(n, w.name)}│${cell(s, w.scope)}│${cell(c, w.schedule)}│`; const line = (l, m, r) => l + '─'.repeat(w.name + 2) + m + '─'.repeat(w.scope + 2) + m + '─'.repeat(w.schedule + 2) + r; console.log(line('┌', '┬', '┐')); console.log(row(H.name, H.scope, H.schedule)); console.log(line('├', '┼', '┤')); jobs.forEach(j => console.log(row(j.name, j.scope, j.schedule))); console.log(line('└', '┴', '┘')); } (async () => { try { await sequelize.authenticate(); console.log('✅ Database connected.'); const cronJobs = [ ...(await startAdminCronJobs()), ...(await startClientCronJobs()), ]; app.listen(PORT, () => { console.log(`🚀 STARR Server running on http://localhost:${PORT}`); console.log(`⚙️ Environment : ${process.env.NODE_ENV}`); console.log('ℹ️ For help, for more information:'); console.log(' Please see this .MD files'); console.log(' 📄 README.md — project setup and overview'); console.log(' 📄 ROUTES.md — all available API routes'); console.log(' 📊 For actual table documentation please see /admin/documentation directory.'); console.log(' 🩺 For actual server situation please go to /api/health and /api/health/ready.'); console.log(''); printCronTable(cronJobs); }); } catch (err) { console.error('❌ Failed to start server:', err); process.exit(1); } })();