bharath/webapp/src/server.js
iamcoolvivek007 e9025a71eb v2.0: Major improvements - Security, Code Quality, UI/UX, Features
Security:
- Add CSRF protection on all forms
- Fix session config (resave:false, saveUninitialized:false)
- Secure cookie settings for production
- Input sanitization middleware
- Request logging middleware
- Security headers via Helmet

Code Quality:
- Async error handling on ALL route handlers
- Proper HTTP status codes (400, 401, 403, 404, 409, 500)
- Input validation on all forms (server-side)
- Username validation (3-30 chars, alphanumeric+underscore)
- Password min length increased to 6
- Generic error messages (no info leakage)
- Graceful shutdown on SIGTERM

UI/UX:
- Dark mode toggle with persistence
- Toast notifications for success/error
- Loading states on form submit
- Improved CSS with CSS variables
- Better desktop responsive design
- New 403 Forbidden page
- Pagination controls
- Improved header with desktop nav

Features:
- Pagination on all list pages (loads, trips, users, messages, etc.)
- Admin stats JSON endpoint
- Admin user delete route
- Load cancel route
- Mark invoice as paid route
- Search/filter preserved on loadboard

Database:
- Additional composite indexes for performance
- Updated timestamps trigger on trips
- Improved FULL migration script

DevEx:
- Development seed script (seed.js)
- Improved Dockerfile (non-root, healthcheck)
- Comprehensive .gitignore
- Updated README v2.0
2026-05-31 18:08:01 +00:00

289 lines
10 KiB
JavaScript

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;