require('dotenv').config(); const express = require('express'); const path = require('path'); const helmet = require('helmet'); const compression = require('compression'); const session = require('express-session'); const cookieParser = require('cookie-parser'); const rateLimit = require('express-rate-limit'); const config = require('./config/env'); const { setupCSRF, validateCSRF, sanitizeBody, requestLogger, asyncHandler } = require('./middleware/security'); const app = express(); // Trust proxy (for rate limiting behind reverse proxy) app.set('trust proxy', 1); // Security headers app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], fontSrc: ["'self'", "https://fonts.gstatic.com"], imgSrc: ["'self'", "data:", "https:"], scriptSrc: ["'self'", "'unsafe-inline'"], connectSrc: ["'self'"], }, }, crossOriginEmbedderPolicy: false, })); app.use(compression()); app.use(requestLogger); // Rate limiting const generalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 200, standardHeaders: true, legacyHeaders: false, message: 'Too many requests, please try again later.', }); app.use(generalLimiter); // Body parsing app.use(express.json({ limit: '1mb' })); app.use(express.urlencoded({ extended: true, limit: '1mb' })); app.use(cookieParser()); // Static files with caching app.use(express.static(path.join(__dirname, 'public'), { maxAge: config.nodeEnv === 'production' ? '1d' : 0, etag: true, })); // View engine app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // Session with secure defaults const isProduction = config.nodeEnv === 'production'; app.use(session({ secret: config.session.secret, resave: false, saveUninitialized: false, cookie: { secure: isProduction, httpOnly: true, sameSite: 'lax', maxAge: 24 * 60 * 60 * 1000, }, name: 'bt.sid', })); // CSRF protection app.use(setupCSRF); app.use(sanitizeBody); // Make user and helpers available to all views app.use((req, res, next) => { res.locals.user = req.session.user || null; res.locals.appName = 'भारत ट्रक्स'; res.locals.appNameEn = 'BharathTrucks'; res.locals.formatINR = require('./lib/india').formatINR; res.locals.query = req.query; next(); }); // i18n const { i18n, LANGS } = require('./middleware/i18n'); app.use(i18n); app.get('/lang/:code', (req, res) => { const code = req.params.code; if (LANGS.includes(code)) req.session.lang = code; req.session.save(() => { res.redirect(req.get('Referer') || '/'); }); }); // CSRF validation for POST/PUT/DELETE app.use(validateCSRF); // Routes const authRoutes = require('./routes/auth'); const loadRoutes = require('./routes/loads'); const tripRoutes = require('./routes/trips'); const adminRoutes = require('./routes/admin'); const messageRoutes = require('./routes/messages'); // Phase 1 routes const whatsappRoutes = require('./routes/whatsapp'); const driverLedgerRoutes = require('./routes/driver-ledger'); const tripplannerRoutes = require('./routes/tripplanner'); const returnloadRoutes = require('./routes/returnload'); const safetyRoutes = require('./routes/safety'); const maintenanceRoutes = require('./routes/maintenance'); const fastagRoutes = require('./routes/fastag'); const notificationsRoutes = require('./routes/notifications'); // Phase 2 routes const gamificationRoutes = require('./routes/gamification'); const referralRoutes = require('./routes/referral'); const feedRoutes = require('./routes/feed'); const leaderboardRoutes = require('./routes/leaderboard'); const challengesRoutes = require('./routes/challenges'); const invoiceRoutes = require('./routes/invoice'); const ratesRoutes = require('./routes/rates'); const sitemapRoutes = require('./routes/sitemap'); // Phase 3 routes const minigamesRoutes = require('./routes/minigames'); const fleetRoutes = require('./routes/fleet'); const classifiedsRoutes = require('./routes/classifieds'); const documentsRoutes = require('./routes/documents'); const bankRoutes = require('./routes/bank'); const searchRoutes = require('./routes/search'); const reportsRoutes = require('./routes/reports'); const newsRoutes = require('./routes/news'); app.use('/', authRoutes); app.use('/loadboard', loadRoutes); app.use('/loadboard', whatsappRoutes); app.use('/trips', tripRoutes); app.use('/admin', adminRoutes); app.use('/messages', messageRoutes); app.use('/driver', driverLedgerRoutes); app.use('/trip-planner', tripplannerRoutes); app.use('/returnload', returnloadRoutes); app.use('/safety', safetyRoutes); app.use('/maintenance', maintenanceRoutes); app.use('/fastag', fastagRoutes); app.use('/notifications', notificationsRoutes); // Phase 2 app.use('/gamification', gamificationRoutes); app.use('/referral', referralRoutes); app.use('/feed', feedRoutes); app.use('/leaderboard', leaderboardRoutes); app.use('/challenges', challengesRoutes); app.use('/invoice', invoiceRoutes); app.use('/rates', ratesRoutes); app.use('/', sitemapRoutes); // Phase 3 app.use('/games', minigamesRoutes); app.use('/fleet', fleetRoutes); app.use('/classifieds', classifiedsRoutes); app.use('/documents', documentsRoutes); app.use('/bank', bankRoutes); app.use('/search', searchRoutes); app.use('/reports', reportsRoutes); app.use('/news', newsRoutes); const { requireAuth, requireDriver, requireShipper, requireBroker } = require('./middleware/auth'); const supabase = require('./services/supabase'); app.get('/health', (req, res) => res.json({ status: 'ok', ts: Date.now() })); app.get('/more', requireAuth, (req, res) => res.render('pages/more')); app.get('/', (req, res) => { if (req.session && req.session.user) { const { ROLES } = require('./config/constants'); if (req.session.user.role === ROLES.DRIVER) return res.redirect('/driver'); if (req.session.user.role === ROLES.SHIPPER) return res.redirect('/shipper'); if (req.session.user.role === ROLES.BROKER) return res.redirect('/broker'); } res.render('pages/landing'); }); // Profile app.get('/profile', requireAuth, asyncHandler(async (req, res) => { const { data: profile } = await supabase.from('app_users').select('*').eq('id', req.session.user.id).single(); res.render('pages/profile', { profile: profile || req.session.user, success: req.query.ok }); })); app.post('/profile', requireAuth, asyncHandler(async (req, res) => { const { name, phone, city, state } = req.body; if (!name || !name.trim()) { const { data: profile } = await supabase.from('app_users').select('*').eq('id', req.session.user.id).single(); return res.render('pages/profile', { profile: profile || req.session.user, error: 'Name is required' }); } await supabase.from('app_users').update({ name: name.trim(), phone: phone || null, city: city || null, state: state || null, }).eq('id', req.session.user.id); req.session.user.name = name.trim(); res.redirect('/profile?ok=1'); })); // Driver dashboard app.get('/driver', requireAuth, requireDriver, asyncHandler(async (req, res) => { const userId = req.session.user.id; const { data: bids } = await supabase.from('bids').select('status').eq('driver_id', userId); const { data: trips } = await supabase.from('trips').select('*, load:load_id(origin_city, destination_city)').eq('driver_id', userId).order('created_at', { ascending: false }); const activeTrips = (trips || []).filter(t => !['delivered', 'cancelled'].includes(t.status)); const delivered = (trips || []).filter(t => t.status === 'delivered'); const earnings = delivered.reduce((s, t) => s + (parseFloat(t.amount) || 0), 0); res.render('pages/driver-dashboard', { stats: { totalTrips: (trips || []).length, activeBids: (bids || []).filter(b => b.status === 'pending').length, earnings }, activeTrips, }); })); // Shipper dashboard app.get('/shipper', requireAuth, requireShipper, asyncHandler(async (req, res) => { const userId = req.session.user.id; const { data: loads } = await supabase.from('loads').select('*').eq('posted_by', userId).order('created_at', { ascending: false }).limit(10); const { data: trips } = await supabase.from('trips').select('status').eq('shipper_id', userId); const allLoads = loads || []; res.render('pages/shipper-dashboard', { stats: { totalLoads: allLoads.length, openLoads: allLoads.filter(l => l.status === 'open').length, activeTrips: (trips || []).filter(t => !['delivered', 'cancelled'].includes(t.status)).length }, recentLoads: allLoads.slice(0, 5), }); })); // Broker dashboard app.get('/broker', requireAuth, requireBroker, asyncHandler(async (req, res) => { const userId = req.session.user.id; const { data: loads } = await supabase.from('loads').select('*').eq('posted_by', userId).order('created_at', { ascending: false }).limit(10); const { data: trips } = await supabase.from('trips').select('status').eq('shipper_id', userId); const allLoads = loads || []; res.render('pages/broker-dashboard', { stats: { totalLoads: allLoads.length, bookedLoads: allLoads.filter(l => l.status === 'booked').length, activeTrips: (trips || []).filter(t => !['delivered', 'cancelled'].includes(t.status)).length }, recentLoads: allLoads.slice(0, 5), }); })); // 404 app.use((req, res) => { res.status(404); if (req.accepts('html')) { res.render('pages/404'); } else { res.json({ error: 'Not found' }); } }); // Global error handler app.use((err, req, res, next) => { console.error(`[ERROR] ${req.method} ${req.url}:`, err.message); if (config.nodeEnv === 'development') console.error(err.stack); res.status(err.status || 500); if (req.accepts('html')) { res.render('pages/500', { error: config.nodeEnv === 'development' ? err.message : null }); } else { res.json({ error: 'Internal server error' }); } }); const server = app.listen(config.port, '::', () => { console.log(`\n🚛 BharathTrucks running at http://localhost:${config.port}`); console.log(` Environment: ${config.nodeEnv}`); console.log(` Press Ctrl+C to stop\n`); }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully...'); server.close(() => { console.log('Server closed.'); process.exit(0); }); }); module.exports = app;