Files
starr-philproperties/server.js
T
2026-07-07 14:02:36 +08:00

194 lines
9.4 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();
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,
cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, sameSite: '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);
}
})();