chore: relocate backend into apps/api ahead of monorepo merge

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 12:20:40 +08:00
co-authored by Claude Sonnet 5
parent af44598df6
commit eaa6d2276a
388 changed files with 0 additions and 13998 deletions
+216
View File
@@ -0,0 +1,216 @@
/***********************************************************************************************************************************************************************
* 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);
}
})();