Files
starr-philproperties/server.js
T
kennethobsequio 2e9c2ad43f fix
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-08-13 19:58:52 +08:00

217 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/***********************************************************************************************************************************************************************
* 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();
// Local-dev-only: stop the process on wake from laptop sleep/idle instead of
// letting node-cron dump a "missed execution" warning per elapsed tick. Must
// start ticking as early as possible so it wins the race against node-cron's
// own heartbeat once cron jobs are registered below. No-ops in production.
require('./utils/suspendGuard.util').startSuspendGuard();
// Force IPv4-only outbound connections. Hosts that resolve AAAA records but
// have no working IPv6 route (e.g. to Google's OAuth endpoints) hit ENETUNREACH
// on the v6 attempt — and Node's dual-stack "Happy Eyeballs" connector
// (autoSelectFamily) throws AggregateError instead of falling back cleanly to
// the reachable IPv4 address. dns.setDefaultResultOrder alone does NOT fix
// this (the connector still races both families); autoSelectFamily has to be
// disabled outright. See [AUTH] googleCallback OIDC error: AxiosError [AggregateError].
require('net').setDefaultAutoSelectFamily(false);
require('dns').setDefaultResultOrder('ipv4first');
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);
}
})();