mirror of
http://forgejo-oa09toasww4dgii9cj3gpzda.187.127.164.61.sslip.io/iamcoolvivek007/bharath.git
synced 2026-06-11 00:06:51 +00:00
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
289 lines
10 KiB
JavaScript
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;
|