Carpool / server /server.js
Shivam
fix express routing crash for path-to-regexp
7b9d315
Raw History Blame Contribute Delete
95.3 kB
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const cors = require('cors');
require('dotenv').config();
const fs = require('fs');
const http = require('http');
const { Server } = require('socket.io');
const multer = require('multer');
const path = require('path');
const app = express();
const port = process.env.PORT || 3001;
// Setup static serving for uploads
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Ensure uploads directory exists
if (!fs.existsSync(path.join(__dirname, 'uploads'))) {
fs.mkdirSync(path.join(__dirname, 'uploads'));
}
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/')
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + Math.round(Math.random() * 1E9) + path.extname(file.originalname))
}
});
const upload = multer({ storage: storage });
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: '*',
methods: ["GET", "POST"]
}
});
app.set('io', io);
io.on('connection', (socket) => {
console.log('A user connected via WebSocket');
// Users can join a room with their user ID to receive targeted notifications
socket.on('join', (userId) => {
socket.join(`user_${userId}`);
console.log(`User ${userId} joined their personal room.`);
});
socket.on('join_admin', () => {
socket.join('admin_room');
console.log('An Admin joined the admin room.');
});
socket.on('disconnect', () => {
console.log('User disconnected');
});
});
// ─── AI Provider Configuration ────────────────────────────────────────────────
// Switch between "nvidia" and "gemini" via the AI_PROVIDER env variable.
const AI_PROVIDER = (process.env.AI_PROVIDER || 'nvidia').toLowerCase();
// ─── Shared System Prompt & Knowledge Base ────────────────────────────────────
let systemPrompt = "You are Commute Copilot, the AI assistant for Reliance Commuter Connect β€” a corporate carpooling platform for Reliance Industries employees. You help users find and book rides, list new commutes, answer questions about personal stats/earnings, and clarify carpool policies. IMPORTANT: Use the draft_booking tool when a user wants to find or book a ride. Use the draft_listing tool when a driver wants to offer a ride. Never invent ride details β€” if origin, destination, or time are unclear, ask the user. When answering stats questions, use the live data in the [System Note] prefix.";
try {
const reqPath = 'C:\\Users\\saxen\\Downloads\\carpool_requirements.md';
if (fs.existsSync(reqPath)) {
systemPrompt += "\n\n--- KNOWLEDGE BASE ---\n" + fs.readFileSync(reqPath, 'utf8');
}
} catch (e) {
console.log('Could not load knowledge base file.');
}
// ─── OpenAI-compatible tool definitions (used by NVIDIA NIM & Gemini adapter) ─
const CARPOOL_TOOLS = [
{
type: 'function',
function: {
name: 'draft_booking',
description: 'Drafts a ride booking request when the user wants to find or book a ride. Call this whenever the user expresses intent to book, search, or request a ride.',
parameters: {
type: 'object',
properties: {
origin: { type: 'string', description: 'Pickup location (street, area or landmark)' },
destination: { type: 'string', description: 'Drop-off location (street, area or landmark)' },
time: { type: 'string', description: 'Desired departure time in HH:MM 24-hour format (e.g., 09:00)' }
},
required: ['origin', 'destination', 'time']
}
}
},
{
type: 'function',
function: {
name: 'draft_listing',
description: 'Drafts a new commute ride listing when a Pool Host (driver) wants to offer seats. Call this when the driver mentions offering a ride, adding a commute, or listing seats.',
parameters: {
type: 'object',
properties: {
origin: { type: 'string', description: 'Start location of the commute' },
destination: { type: 'string', description: 'End location of the commute' },
time: { type: 'string', description: 'Departure time in HH:MM 24-hour format (e.g., 08:30)' },
seats: { type: 'integer', description: 'Number of seats being offered (1–6)' },
price: { type: 'integer', description: 'Price per seat in INR (e.g., 50)' }
},
required: ['origin', 'destination', 'time', 'seats', 'price']
}
}
}
];
// ─── NVIDIA NIM Client (OpenAI-compatible) ────────────────────────────────────
let nvidiaClient = null;
const NVIDIA_MODEL = process.env.NVIDIA_MODEL || 'meta/llama-3.3-70b-instruct';
if (AI_PROVIDER === 'nvidia') {
if (!process.env.NVIDIA_API_KEY || process.env.NVIDIA_API_KEY === 'nvapi-YOUR_KEY_HERE') {
console.warn('[AI] ⚠️ NVIDIA_API_KEY not configured. AI Copilot will be unavailable.');
console.warn('[AI] Get your key at https://build.nvidia.com β†’ "Get API Key"');
} else {
try {
const OpenAI = require('openai');
nvidiaClient = new OpenAI({
apiKey: process.env.NVIDIA_API_KEY,
baseURL: 'https://integrate.api.nvidia.com/v1',
});
console.log(`[AI] βœ… NVIDIA NIM ready β€” model: ${NVIDIA_MODEL}`);
} catch (e) {
console.error('[AI] Failed to initialise NVIDIA client:', e.message);
}
}
}
// ─── Google Gemini Client (fallback / legacy) ─────────────────────────────────
let geminiModel = null;
if (AI_PROVIDER === 'gemini' || !nvidiaClient) {
try {
const { GoogleGenerativeAI } = require('@google/generative-ai');
const genAI = process.env.GEMINI_API_KEY ? new GoogleGenerativeAI(process.env.GEMINI_API_KEY) : null;
if (genAI) {
geminiModel = genAI.getGenerativeModel({
model: 'gemini-2.5-flash-lite',
systemInstruction: systemPrompt,
tools: [{
functionDeclarations: CARPOOL_TOOLS.map(t => ({
name: t.function.name,
description: t.function.description,
parameters: {
type: 'OBJECT',
properties: Object.fromEntries(
Object.entries(t.function.parameters.properties).map(([k, v]) => [
k, { type: v.type.toUpperCase(), description: v.description }
])
),
required: t.function.parameters.required
}
}))
}]
});
console.log(`[AI] βœ… Google Gemini ready (fallback) β€” model: gemini-2.5-flash-lite`);
}
} catch(e) {
console.warn('[AI] Gemini SDK not available:', e.message);
}
}
// Convenience: which AI is active?
const activeAI = nvidiaClient ? 'nvidia' : (geminiModel ? 'gemini' : null);
if (!activeAI) console.warn('[AI] ⚠️ No AI provider configured. /api/chat will return 500.');
else console.log(`[AI] Active provider: ${activeAI.toUpperCase()}`);
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');
const webpush = require('web-push');
if (process.env.VAPID_PUBLIC_KEY && process.env.VAPID_PRIVATE_KEY) {
webpush.setVapidDetails(
process.env.VAPID_EMAIL || 'mailto:admin@reliance.com',
process.env.VAPID_PUBLIC_KEY,
process.env.VAPID_PRIVATE_KEY
);
}
// Push notification helper
const sendPushToUser = (userId, title, body, url = '/') => {
if (!process.env.VAPID_PUBLIC_KEY) return;
db.all(`SELECT * FROM push_subscriptions WHERE user_id = ?`, [userId], (err, subscriptions) => {
if (err || !subscriptions) return;
const payload = JSON.stringify({ title, body, url });
subscriptions.forEach(sub => {
const pushSubscription = {
endpoint: sub.endpoint,
keys: { p256dh: sub.p256dh, auth: sub.auth }
};
webpush.sendNotification(pushSubscription, payload).catch(error => {
if (error.statusCode === 410 || error.statusCode === 404) {
db.run(`DELETE FROM push_subscriptions WHERE endpoint = ?`, [sub.endpoint]);
}
});
});
});
};
// Strict Secret Check
if (!process.env.JWT_SECRET) {
console.error("FATAL ERROR: JWT_SECRET is not defined in the environment. Please add JWT_SECRET to your .env file.");
process.exit(1);
}
// Rate limiting middleware
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: { error: 'Too many requests, please try again later.' }
});
// Middleware
app.use(cors({
origin: '*',
exposedHeaders: ['X-New-Token']
}));
app.use(express.json());
app.use('/api/', apiLimiter);
const authenticateToken = (req, res, next) => {
// Allow unauthenticated access to login, register, forgot-password, and reset-password
if (req.path === '/auth/login' || req.path === '/register' || req.path === '/auth/forgot-password' || req.path === '/auth/reset-password') {
return next();
}
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Access token required.' });
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(401).json({ error: 'Invalid or expired token. Please log in again.' });
// Refresh token on activity (sliding session)
const newToken = jwt.sign(
{ id: user.id, email: user.email, role: user.role, name: user.name, gender: user.gender },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.setHeader('X-New-Token', newToken);
req.user = user;
next();
});
};
app.use('/api/', authenticateToken);
// Database setup
const db = new sqlite3.Database('./database.sqlite', (err) => {
if (err) {
console.error('Error connecting to database:', err.message);
} else {
console.log('Connected to the SQLite database.');
initializeSchema();
}
});
function initializeSchema() {
db.serialize(() => {
// Users Table
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL,
emp_id TEXT,
gender TEXT,
password TEXT,
role TEXT NOT NULL CHECK(role IN ('Pool Host', 'Passenger', 'Admin')),
has_vehicle_pass BOOLEAN DEFAULT 0,
vehicle_no TEXT,
vehicle_make TEXT,
vehicle_model TEXT,
vehicle_capacity TEXT,
status TEXT DEFAULT 'Active',
no_show_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(email, role)
)`, [], (err) => {
if (!err) {
db.serialize(() => {
db.run(`ALTER TABLE users ADD COLUMN status TEXT DEFAULT 'Active'`, [], () => {});
db.run(`ALTER TABLE users ADD COLUMN no_show_count INTEGER DEFAULT 0`, [], () => {});
db.run(`ALTER TABLE users ADD COLUMN emp_id TEXT`, [], () => {});
db.run(`ALTER TABLE users ADD COLUMN gender TEXT`, [], () => {});
db.run(`ALTER TABLE users ADD COLUMN avatar_url TEXT`, [], () => {});
db.run(`ALTER TABLE users ADD COLUMN penalty_points INTEGER DEFAULT 0`, [], () => {});
db.run(`ALTER TABLE users ADD COLUMN suspended_until DATETIME`, [], () => {});
db.run(`ALTER TABLE users ADD COLUMN password TEXT`, [], () => {
// Seed default passwords for existing users
const defaultHash = '$2b$10$RmRqG9A5Q68w4jGd1L4YjOCD/z76Sb/O1V4eH9eVKyvyf.npBA/AO'; // Password123!
db.run(`UPDATE users SET password = ? WHERE password IS NULL OR password = ''`, [defaultHash]);
// Seed Admin User
const adminHash = '$2b$10$kzOslSASLTVaqbM0mRtbDOQQJOARQTLlwzmvtuafDMnHSawIEvugK'; // Reliance@1024
db.run(`INSERT OR IGNORE INTO users (name, email, password, role, status) VALUES (?, ?, ?, ?, ?)`, ['Admin Commute Controller', 'admin@reliance.com', adminHash, 'Admin', 'Active']);
});
});
}
});
// Password Resets Table
db.run(`CREATE TABLE IF NOT EXISTS password_resets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
token TEXT NOT NULL,
expires_at DATETIME NOT NULL,
used BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
// Rides Table
db.run(`CREATE TABLE IF NOT EXISTS rides (
id INTEGER PRIMARY KEY AUTOINCREMENT,
driver_id INTEGER NOT NULL,
origin TEXT NOT NULL,
destination TEXT NOT NULL,
origin_lat REAL,
origin_lng REAL,
dest_lat REAL,
dest_lng REAL,
route_polyline TEXT,
departure_time TEXT NOT NULL,
recurring_days TEXT DEFAULT 'Mon,Tue,Wed,Thu,Fri',
seats_offered INTEGER,
original_seats INTEGER,
price_per_seat REAL,
women_only BOOLEAN DEFAULT 0,
status TEXT DEFAULT 'Scheduled',
expected_duration_mins INTEGER,
actual_start_time DATETIME,
delay_reminder_sent INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (driver_id) REFERENCES users (id)
)`, [], (err) => {
if (!err) {
// Safe Alter for new columns
db.run(`ALTER TABLE rides ADD COLUMN expected_duration_mins INTEGER`, () => {});
db.run(`ALTER TABLE rides ADD COLUMN actual_start_time DATETIME`, () => {});
db.run(`ALTER TABLE rides ADD COLUMN delay_reminder_sent INTEGER DEFAULT 0`, () => {});
db.run(`ALTER TABLE rides ADD COLUMN women_only BOOLEAN DEFAULT 0`, [], () => {});
db.run(`ALTER TABLE rides ADD COLUMN original_seats INTEGER`, [], () => {
db.run(`UPDATE rides SET original_seats = seats_offered WHERE original_seats IS NULL`);
});
db.run(`ALTER TABLE rides ADD COLUMN route_polyline TEXT`, [], () => {});
db.run(`ALTER TABLE rides ADD COLUMN recurring_days TEXT DEFAULT 'Mon,Tue,Wed,Thu,Fri'`, [], () => {});
}
});
// RideRequests Table
db.run(`CREATE TABLE IF NOT EXISTS ride_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ride_id INTEGER,
passenger_listing_id INTEGER,
passenger_id INTEGER NOT NULL,
requested_days TEXT DEFAULT 'Mon,Tue,Wed,Thu,Fri',
status TEXT DEFAULT 'Pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (ride_id) REFERENCES rides (id),
FOREIGN KEY (passenger_id) REFERENCES users (id)
)`, [], (err) => {
if (!err) {
db.run(`ALTER TABLE ride_requests ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP`, [], () => {});
db.run(`ALTER TABLE ride_requests ADD COLUMN passenger_listing_id INTEGER`, [], () => {});
db.run(`ALTER TABLE ride_requests ADD COLUMN requested_days TEXT DEFAULT 'Mon,Tue,Wed,Thu,Fri'`, [], () => {});
db.run(`ALTER TABLE ride_requests ADD COLUMN pickup_lat REAL`, [], () => {});
db.run(`ALTER TABLE ride_requests ADD COLUMN pickup_lng REAL`, [], () => {});
}
});
// Passenger Listings Table (Reverse Matching)
db.run(`CREATE TABLE IF NOT EXISTS passenger_listings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
passenger_id INTEGER NOT NULL,
origin TEXT NOT NULL,
destination TEXT NOT NULL,
origin_lat REAL,
origin_lng REAL,
dest_lat REAL,
dest_lng REAL,
departure_time TEXT NOT NULL,
recurring_days TEXT DEFAULT 'Mon,Tue,Wed,Thu,Fri',
status TEXT DEFAULT 'Active',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (passenger_id) REFERENCES users (id)
)`);
// Trip Logs Table (Daily records for recurring rides)
db.run(`CREATE TABLE IF NOT EXISTS trip_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ride_id INTEGER NOT NULL,
date TEXT NOT NULL,
status TEXT DEFAULT 'Completed',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (ride_id) REFERENCES rides (id),
UNIQUE(ride_id, date)
)`);
// Incidents Table
db.run(`CREATE TABLE IF NOT EXISTS incidents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reported_by INTEGER NOT NULL,
type TEXT NOT NULL,
status TEXT DEFAULT 'Open',
sla_deadline DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (reported_by) REFERENCES users (id)
)`);
// Notifications Table
db.run(`CREATE TABLE IF NOT EXISTS notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
message TEXT NOT NULL,
type TEXT NOT NULL,
action_data TEXT,
is_read BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
// Messages Table
db.run(`CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id INTEGER NOT NULL,
sender_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (request_id) REFERENCES ride_requests(id),
FOREIGN KEY (sender_id) REFERENCES users(id)
)`);
// Ratings Table
db.run(`CREATE TABLE IF NOT EXISTS ratings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
trip_id INTEGER NOT NULL,
rater_id INTEGER NOT NULL,
ratee_id INTEGER NOT NULL,
stars INTEGER NOT NULL CHECK(stars >= 1 AND stars <= 5),
tags TEXT,
comment TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (trip_id) REFERENCES rides(id),
FOREIGN KEY (rater_id) REFERENCES users(id),
FOREIGN KEY (ratee_id) REFERENCES users(id)
)`);
// Push Subscriptions Table
db.run(`CREATE TABLE IF NOT EXISTS push_subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
endpoint TEXT NOT NULL UNIQUE,
p256dh TEXT NOT NULL,
auth TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)`);
});
}
// Haversine formula in KM
function getDistanceFromLatLonInKm(lat1, lon1, lat2, lon2) {
var R = 6371; // Radius of the earth in km
var dLat = deg2rad(lat2-lat1);
var dLon = deg2rad(lon2-lon1);
var a =
Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2)
;
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var d = R * c; // Distance in km
return d;
}
function deg2rad(deg) {
return deg * (Math.PI/180)
}
// API Endpoints
// GET VAPID public key
app.get('/api/push/vapid-public-key', (req, res) => {
res.json({ publicKey: process.env.VAPID_PUBLIC_KEY });
});
// POST save subscription
app.post('/api/push/subscribe', (req, res) => {
const { endpoint, keys } = req.body;
if (!endpoint || !keys) return res.status(400).json({ error: 'Invalid subscription object' });
db.run(
`INSERT OR IGNORE INTO push_subscriptions (user_id, endpoint, p256dh, auth) VALUES (?, ?, ?, ?)`,
[req.user.id, endpoint, keys.p256dh, keys.auth],
function(err) {
if (err) return res.status(500).json({ error: 'Failed to save subscription' });
res.status(201).json({ message: 'Subscribed successfully' });
}
);
});
// DELETE remove subscription
app.delete('/api/push/subscribe', (req, res) => {
const { endpoint } = req.body;
if (!endpoint) return res.status(400).json({ error: 'Endpoint required' });
db.run(`DELETE FROM push_subscriptions WHERE user_id = ? AND endpoint = ?`, [req.user.id, endpoint], function(err) {
if (err) return res.status(500).json({ error: 'Failed to remove subscription' });
res.json({ message: 'Unsubscribed successfully' });
});
});
async function recalculateMultiStopRoute(rideId, io) {
db.get('SELECT * FROM rides WHERE id=?', [rideId], async (err, ride) => {
if (!ride) return;
// Fetch accepted passengers with pickup coords
db.all(
`SELECT rr.pickup_lat, rr.pickup_lng FROM ride_requests rr
WHERE rr.ride_id=? AND rr.status='Accepted'
AND rr.pickup_lat IS NOT NULL ORDER BY rr.created_at ASC`,
[rideId],
async (err, stops) => {
// Build waypoints: driver_origin -> passenger stops -> destination
const waypoints = [
`${ride.origin_lng},${ride.origin_lat}`,
...stops.map(s => `${s.pickup_lng},${s.pickup_lat}`),
`${ride.dest_lng},${ride.dest_lat}`
].join(';');
try {
const r = await fetch(
`http://router.project-osrm.org/route/v1/driving/${waypoints}?geometries=geojson&overview=full`
);
if (!r.ok) return;
const data = await r.json();
if (!data.routes?.length) return;
const polyline = JSON.stringify(data.routes[0].geometry.coordinates);
const duration = Math.ceil(data.routes[0].duration / 60);
db.run('UPDATE rides SET route_polyline=?, expected_duration_mins=? WHERE id=?',
[polyline, duration, rideId]);
// Emit new route to driver
io.to(`user_${ride.driver_id}`).emit('route_updated', {
ride_id: rideId, route_polyline: polyline, duration_mins: duration
});
} catch (e) { console.error('Multi-stop OSRM error', e); }
}
);
});
}
// Get all roles for an email
app.get('/api/users/roles', (req, res) => {
const email = req.query.email;
if (!email) return res.status(400).json({ error: 'Email is required' });
db.all(`SELECT DISTINCT role FROM users WHERE email = ?`, [email], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows.map(r => r.role));
});
});
// Get all users
app.get('/api/users', (req, res) => {
if (req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Access denied: Admin privileges required.' });
}
db.all(`SELECT * FROM users ORDER BY created_at DESC`, [], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});
// Admin: Assign Admin Role
app.post('/api/users/:id/assign-admin', (req, res) => {
if (req.user.role !== 'Admin') return res.status(403).json({ error: 'Admin only' });
const userId = req.params.id;
db.run(`UPDATE users SET role = 'Admin' WHERE id = ?`, [userId], function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ message: 'User granted Admin privileges successfully' });
});
});
// Admin: Verify user (grant vehicle pass)
app.post('/api/users/:id/verify', (req, res) => {
if (req.user.role !== 'Admin') return res.status(403).json({ error: 'Admin only' });
const userId = req.params.id;
db.run(`UPDATE users SET has_vehicle_pass = 1 WHERE id = ?`, [userId], function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ message: 'User verified successfully' });
});
});
// Toggle user status (Active / Suspended)
app.post('/api/users/:id/toggle-status', (req, res) => {
if (req.user.role !== 'Admin') return res.status(403).json({ error: 'Admin only' });
const userId = req.params.id;
db.get(`SELECT status FROM users WHERE id = ?`, [userId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'User not found' });
const newStatus = row.status === 'Active' ? 'Suspended' : 'Active';
db.run(`UPDATE users SET status = ? WHERE id = ?`, [newStatus, userId], function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ message: `User status updated to ${newStatus}`, status: newStatus });
});
});
});
// Update user profile details
app.post('/api/users/:id/update', (req, res) => {
const userId = req.params.id;
if (req.user.id !== parseInt(userId) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to modify this profile' });
}
const { name, vehicle_no, vehicle_make, vehicle_model, vehicle_capacity } = req.body;
if (!name || name.length > 100) {
return res.status(400).json({ error: 'Valid name is required' });
}
db.run(`
UPDATE users
SET name = ?, vehicle_no = ?, vehicle_make = ?, vehicle_model = ?, vehicle_capacity = ?
WHERE id = ?
`, [name, vehicle_no || null, vehicle_make || null, vehicle_model || null, vehicle_capacity || null, userId], function(err) {
if (err) return res.status(500).json({ error: err.message });
// Fetch and return the updated user object
db.get(`SELECT * FROM users WHERE id = ?`, [userId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
res.json({ message: 'Profile updated successfully', user: row });
});
});
});
// Login endpoint with bcrypt and JWT
app.post('/api/auth/login', (req, res) => {
const { email, password, role } = req.body;
if (!email || !password || !role) {
return res.status(400).json({ error: 'Email, password, and role are required' });
}
db.get(`SELECT * FROM users WHERE email = ? AND role = ?`, [email, role], async (err, user) => {
if (err) return res.status(500).json({ error: err.message });
if (!user) return res.status(404).json({ error: 'User not found. Please register first.' });
const processLogin = async () => {
try {
const match = await bcrypt.compare(password, user.password);
if (!match) {
return res.status(401).json({ error: 'Invalid password' });
}
// Generate JWT
const token = jwt.sign(
{ id: user.id, email: user.email, role: user.role, name: user.name, gender: user.gender },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
// Don't send password hash back to client
delete user.password;
res.json({ user, token });
} catch (compareErr) {
console.error(compareErr);
res.status(500).json({ error: 'Authentication processing failed' });
}
};
if (user.status === 'Suspended') {
if (user.suspended_until && new Date(user.suspended_until) < new Date()) {
db.run(`UPDATE users SET status = 'Active', penalty_points = 0, suspended_until = NULL WHERE id = ?`, [user.id], (err) => {
if (err) return res.status(500).json({ error: 'Failed to lift suspension' });
user.status = 'Active';
user.penalty_points = 0;
user.suspended_until = null;
processLogin();
});
} else {
const msg = user.suspended_until ? `Account suspended until ${new Date(user.suspended_until).toLocaleString()}.` : 'Account suspended.';
return res.status(403).json({ status: 'Suspended', error: msg });
}
} else {
processLogin();
}
});
});
// Forgot Password Endpoint
app.post('/api/auth/forgot-password', (req, res) => {
const { email } = req.body;
if (!email) return res.status(400).json({ error: 'Email is required' });
db.get(`SELECT id FROM users WHERE email = ?`, [email], (err, user) => {
if (err) return res.status(500).json({ error: err.message });
if (!user) {
// Return success even if not found to prevent email enumeration
return res.json({ message: 'If an account with that email exists, a reset token has been generated.' });
}
const token = require('crypto').randomBytes(3).toString('hex').toUpperCase();
const expiresAt = new Date(Date.now() + 15 * 60000).toISOString(); // 15 mins
db.run(`INSERT INTO password_resets (email, token, expires_at) VALUES (?, ?, ?)`, [email, token, expiresAt], function(err) {
if (err) return res.status(500).json({ error: err.message });
console.log(`\n\n=== PASSWORD RESET TOKEN FOR ${email}: ${token} ===\n\n`);
res.json({ message: 'If an account with that email exists, a reset token has been generated. Check backend console.' });
});
});
});
// Reset Password Endpoint
app.post('/api/auth/reset-password', (req, res) => {
const { email, token, newPassword } = req.body;
if (!email || !token || !newPassword) {
return res.status(400).json({ error: 'Email, token, and new password are required' });
}
if (newPassword.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters long' });
}
// Find valid token
db.get(`SELECT * FROM password_resets WHERE email = ? AND token = ? AND used = 0 AND expires_at > datetime('now')`, [email, token], async (err, resetRecord) => {
if (err) return res.status(500).json({ error: err.message });
if (!resetRecord) {
return res.status(400).json({ error: 'Invalid or expired reset token.' });
}
try {
const hashedPassword = await bcrypt.hash(newPassword, 10);
db.run(`UPDATE users SET password = ? WHERE email = ?`, [hashedPassword, email], function(err) {
if (err) return res.status(500).json({ error: 'Failed to update password' });
// Mark token as used
db.run(`UPDATE password_resets SET used = 1 WHERE id = ?`, [resetRecord.id]);
res.json({ message: 'Password has been successfully reset. You can now log in.' });
});
} catch (hashErr) {
res.status(500).json({ error: 'Failed to process new password' });
}
});
});
// Avatar Upload Endpoint
app.post('/api/users/:id/avatar', upload.single('avatar'), (req, res) => {
const userId = req.params.id;
if (req.user.id !== parseInt(userId) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
if (!req.file) {
return res.status(400).json({ error: 'No image file uploaded' });
}
const avatarUrl = `/uploads/${req.file.filename}`;
db.run(`UPDATE users SET avatar_url = ? WHERE id = ?`, [avatarUrl, userId], function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ message: 'Avatar updated successfully', avatar_url: avatarUrl });
});
});
// Pool Host: get listed rides with pending and accepted passenger counts
app.get('/api/users/:id/rides', (req, res) => {
const driverId = req.params.id;
const today = new Date().toISOString().split('T')[0];
if (req.user.id !== parseInt(driverId) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
db.all(`
SELECT r.*,
COALESCE(tl.status, r.status) as status,
(SELECT COUNT(*) FROM ride_requests rr WHERE rr.ride_id = r.id AND rr.status = 'Pending') as pending_requests,
(SELECT COUNT(*) FROM ride_requests rr WHERE rr.ride_id = r.id AND rr.status = 'Accepted') as accepted_requests
FROM rides r
LEFT JOIN trip_logs tl ON r.id = tl.ride_id AND tl.date = ?
WHERE r.driver_id = ?
ORDER BY r.created_at DESC
`, [today, driverId], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
// For each ride, fetch passenger requests details
const promises = rows.map(ride => {
return new Promise((resolve, reject) => {
db.all(`
SELECT rr.id as request_id, rr.status as request_status, rr.created_at as requested_at,
u.id as passenger_id, u.name as passenger_name, u.email as passenger_email, u.avatar_url as passenger_avatar_url
FROM ride_requests rr
JOIN users u ON rr.passenger_id = u.id
WHERE rr.ride_id = ?
`, [ride.id], (err, requests) => {
if (err) reject(err);
else {
ride.requests = requests;
resolve(ride);
}
});
});
});
Promise.all(promises)
.then(completedRides => res.json(completedRides))
.catch(err => res.status(500).json({ error: err.message }));
});
});
// Passenger: get request history
app.get('/api/users/:id/requests', (req, res) => {
const passengerId = req.params.id;
const today = new Date().toISOString().split('T')[0];
if (req.user.id !== parseInt(passengerId) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
db.all(`
SELECT rr.id as request_id, rr.status as request_status, rr.created_at as requested_at,
r.id as ride_id, r.origin, r.destination, r.departure_time, r.price_per_seat,
COALESCE(tl.status, r.status) as ride_status,
u.name as driver_name, u.avatar_url as driver_avatar_url, u.vehicle_no, u.vehicle_make, u.vehicle_model
FROM ride_requests rr
JOIN rides r ON rr.ride_id = r.id
LEFT JOIN trip_logs tl ON r.id = tl.ride_id AND tl.date = ?
JOIN users u ON r.driver_id = u.id
WHERE rr.passenger_id = ?
ORDER BY rr.created_at DESC
`, [today, passengerId], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});
// Passenger: submit ride request OR Host: offer ride
app.post('/api/rides/request', (req, res) => {
const { ride_id, passenger_id, passenger_listing_id, request_type, pickup_lat, pickup_lng } = req.body;
if (!ride_id || !passenger_id) {
return res.status(400).json({ error: 'ride_id and passenger_id are required' });
}
if (request_type !== 'HostOffer' && req.user.id !== parseInt(passenger_id) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
// Check if request already exists
db.get(`SELECT id FROM ride_requests WHERE ride_id = ? AND passenger_id = ?`, [ride_id, passenger_id], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (row) return res.status(409).json({ error: 'A request/offer already exists.' });
// Verify ride has seats and is Scheduled
db.get(`SELECT driver_id, seats_offered, status FROM rides WHERE id = ?`, [ride_id], (err, ride) => {
if (err) return res.status(500).json({ error: err.message });
if (!ride) return res.status(404).json({ error: 'Ride not found.' });
if (ride.status !== 'Scheduled') return res.status(400).json({ error: 'Ride is no longer active.' });
if (ride.seats_offered <= 0) return res.status(400).json({ error: 'No seats available.' });
if (request_type === 'HostOffer' && req.user.id !== ride.driver_id && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to offer this ride.' });
}
const initialStatus = request_type === 'HostOffer' ? 'Offered' : 'Pending';
const stmt = db.prepare(`INSERT INTO ride_requests (ride_id, passenger_id, passenger_listing_id, pickup_lat, pickup_lng, status) VALUES (?, ?, ?, ?, ?, ?)`);
stmt.run([ride_id, passenger_id, passenger_listing_id || null, pickup_lat || null, pickup_lng || null, initialStatus], function(err) {
if (err) return res.status(500).json({ error: err.message });
if (request_type === 'HostOffer') {
req.app.get('io').to(`user_${passenger_id}`).emit('ride_offered', { message: 'A host has offered to pick you up!', ride_id });
sendPushToUser(passenger_id, 'Ride Offered', 'A host has offered to pick you up!');
} else {
req.app.get('io').to(`user_${ride.driver_id}`).emit('ride_requested', { message: 'You have a new ride request!', ride_id });
sendPushToUser(ride.driver_id, 'New Ride Request', 'You have a new ride request!');
}
res.status(201).json({ message: 'Request submitted successfully', requestId: this.lastID });
});
stmt.finalize();
});
});
});
// Pool Host: cancel ride
app.post('/api/rides/:id/cancel', (req, res) => {
const rideId = req.params.id;
db.get(`SELECT * FROM rides WHERE id = ?`, [rideId], (err, cancelledRide) => {
if (err || !cancelledRide) return res.status(500).json({ error: 'Failed to find ride.' });
if (cancelledRide.driver_id !== req.user.id && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to cancel this ride' });
}
// Time-check logic: Block cancellation within 15 mins of departure
const now = new Date();
const current_minutes = now.getHours() * 60 + now.getMinutes();
const [c_hh, c_mm] = cancelledRide.departure_time.split(':').map(Number);
const cancelled_minutes = c_hh * 60 + c_mm;
if (cancelled_minutes - current_minutes >= 0 && cancelled_minutes - current_minutes <= 15) {
return res.status(403).json({ error: 'Cancellation is blocked within 15 minutes of departure.' });
}
db.run(`UPDATE rides SET status = 'Cancelled' WHERE id = ?`, [rideId], function(err) {
if (err) return res.status(500).json({ error: err.message });
// Get all passengers with Pending or Accepted requests
db.all(`SELECT passenger_id FROM ride_requests WHERE ride_id = ? AND status IN ('Pending', 'Accepted')`, [rideId], (err, passengers) => {
if (err) console.error('Failed to get affected passengers:', err.message);
// Auto-reject any Pending or Accepted requests
db.run(`UPDATE ride_requests SET status = 'Rejected' WHERE ride_id = ? AND status IN ('Pending', 'Accepted')`, [rideId], (err) => {
if (err) console.error('Failed to cancel requests:', err.message);
if (passengers && passengers.length > 0) {
// Convert cancelled ride departure time to minutes
const [c_hh, c_mm] = cancelledRide.departure_time.split(':').map(Number);
const cancelled_minutes = c_hh * 60 + c_mm;
// Fetch alternative Scheduled rides
db.all(`
SELECT r.*, u.name as driver_name, u.vehicle_make, u.vehicle_model, u.vehicle_no
FROM rides r
JOIN users u ON r.driver_id = u.id
WHERE r.status = 'Scheduled' AND r.seats_offered > 0 AND r.id != ?
`, [rideId], (err, allActiveRides) => {
if (err || !allActiveRides) return;
passengers.forEach(p => {
let matches = [];
// Helper to find matches within a time deviation window
const findMatches = (minDev, maxDev) => {
let found = [];
allActiveRides.forEach(ride => {
const [r_hh, r_mm] = ride.departure_time.split(':').map(Number);
const ride_minutes = r_hh * 60 + r_mm;
const diff = ride_minutes - cancelled_minutes;
if (diff >= minDev && diff <= maxDev) {
const distOrigin = getDistanceFromLatLonInKm(cancelledRide.origin_lat, cancelledRide.origin_lng, ride.origin_lat, ride.origin_lng);
const distDest = getDistanceFromLatLonInKm(cancelledRide.dest_lat, cancelledRide.dest_lng, ride.dest_lat, ride.dest_lng);
if (distOrigin <= 3 && distDest <= 3) {
found.push({
...ride,
dist_origin_km: parseFloat(distOrigin.toFixed(2)),
dist_dest_km: parseFloat(distDest.toFixed(2)),
total_dist_km: parseFloat((distOrigin + distDest).toFixed(2)),
time_deviation_mins: diff
});
}
}
});
return found;
};
// Phase 1: Try +/- 15 mins
matches = findMatches(-15, 15);
if (matches.length === 0) {
// Phase 2: Try -15 to +30 mins
matches = findMatches(-15, 30);
}
if (matches.length > 0) {
matches.sort((a, b) => a.total_dist_km - b.total_dist_km);
// Insert notification
const actionData = JSON.stringify(matches);
const msg = `Your ride was cancelled by the driver. We found ${matches.length} alternative ride(s) near your original time.`;
db.run(`INSERT INTO notifications (user_id, message, type, action_data) VALUES (?, ?, 'RideCancelled_Alternatives', ?)`, [p.passenger_id, msg, actionData]);
}
});
});
}
res.json({ message: 'Ride successfully cancelled.' });
});
});
});
});
});
// Pool Host: complete ride (Daily Log)
app.post('/api/rides/:id/complete', (req, res) => {
const rideId = req.params.id;
const today = new Date().toISOString().split('T')[0];
db.get(`SELECT driver_id FROM rides WHERE id = ?`, [rideId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Ride not found' });
db.get(`SELECT id FROM ride_requests WHERE ride_id = ? AND passenger_id = ? AND status = 'Accepted'`, [rideId, req.user.id], (err2, reqRow) => {
if (row.driver_id !== req.user.id && req.user.role !== 'Admin' && !reqRow) {
return res.status(403).json({ error: 'Unauthorized to complete this ride' });
}
db.run(`INSERT INTO trip_logs (ride_id, date, status) VALUES (?, ?, 'Completed')
ON CONFLICT(ride_id, date) DO UPDATE SET status = 'Completed'`, [rideId, today], function(err) {
if (err && err.message.includes('syntax error')) {
db.run(`UPDATE trip_logs SET status = 'Completed' WHERE ride_id = ? AND date = ?`, [rideId, today]);
}
// Broadcast ride completion event to driver and all passengers
db.all(`SELECT passenger_id FROM ride_requests WHERE ride_id = ? AND status = 'Accepted'`, [rideId], (err, passengers) => {
if (!err) {
req.app.get('io').to(`user_${row.driver_id}`).emit('ride_completed', { ride_id: rideId });
passengers.forEach(p => {
req.app.get('io').to(`user_${p.passenger_id}`).emit('ride_completed', { ride_id: rideId });
});
}
});
res.json({ message: 'Ride successfully completed for today.' });
});
});
});
});
// Pool Host: notify passengers
// Pool Host: live location update
const last_osrm_check = {};
app.post('/api/rides/:id/location', (req, res) => {
const rideId = req.params.id;
const { lat, lng } = req.body;
db.get(`SELECT driver_id, dest_lat, dest_lng FROM rides WHERE id = ?`, [rideId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Ride not found' });
if (row.driver_id !== req.user.id && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
const now = Date.now();
const shouldCheckOsrm = !last_osrm_check[rideId] || (now - last_osrm_check[rideId] > 60000);
db.all(`
SELECT rr.passenger_id, pl.dest_lat, pl.dest_lng
FROM ride_requests rr
LEFT JOIN passenger_listings pl ON rr.passenger_listing_id = pl.id
WHERE rr.ride_id = ? AND rr.status = 'Accepted'
`, [rideId], async (err, passengers) => {
let driverEta = null;
let passengerEtas = {};
if (shouldCheckOsrm) {
last_osrm_check[rideId] = now;
try {
// Driver ETA
const osrmRes = await fetch(`http://router.project-osrm.org/route/v1/driving/${lng},${lat};${row.dest_lng},${row.dest_lat}`);
if (osrmRes.ok) {
const data = await osrmRes.json();
if (data.routes && data.routes.length > 0) driverEta = Math.ceil(data.routes[0].duration / 60);
}
// Passenger ETAs
for (let p of passengers || []) {
const pDestLat = p.dest_lat || row.dest_lat;
const pDestLng = p.dest_lng || row.dest_lng;
const pRes = await fetch(`http://router.project-osrm.org/route/v1/driving/${lng},${lat};${pDestLng},${pDestLat}`);
if (pRes.ok) {
const pData = await pRes.json();
if (pData.routes && pData.routes.length > 0) passengerEtas[p.passenger_id] = Math.ceil(pData.routes[0].duration / 60);
}
}
} catch (e) {
console.error("OSRM ETA fetch error:", e);
}
}
// Emit driver location & ETA
req.app.get('io').to(`user_${row.driver_id}`).emit('eta_update', { eta_mins: driverEta });
// Broadcast to passengers
if (!err && passengers) {
passengers.forEach(p => {
req.app.get('io').to(`user_${p.passenger_id}`).emit('location_update', {
ride_id: rideId,
lat,
lng,
eta_mins: passengerEtas[p.passenger_id] !== undefined ? passengerEtas[p.passenger_id] : driverEta
});
});
}
res.json({ message: 'Location broadcasted.' });
});
});
});
app.post('/api/rides/:id/notify', (req, res) => {
const rideId = req.params.id;
db.get(`SELECT driver_id, departure_time, status FROM rides WHERE id = ?`, [rideId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Ride not found' });
if (row.driver_id !== req.user.id && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
if (row.status !== 'Scheduled') {
return res.status(400).json({ error: 'Ride is not in Scheduled state' });
}
// Enforce +/- 10 minutes window
const now = new Date();
const current_minutes = now.getHours() * 60 + now.getMinutes();
const [d_hh, d_mm] = row.departure_time.split(':').map(Number);
const departure_minutes = d_hh * 60 + d_mm;
if (Math.abs(departure_minutes - current_minutes) > 10) {
return res.status(400).json({ error: 'You can only notify passengers within 10 minutes of the scheduled departure time.' });
}
db.all(`SELECT passenger_id FROM ride_requests WHERE ride_id = ? AND status = 'Accepted'`, [rideId], (err, passengers) => {
if (err) return res.status(500).json({ error: err.message });
passengers.forEach(p => {
req.app.get('io').to(`user_${p.passenger_id}`).emit('host_arriving', { message: 'Your Pool Host will be arriving soon. Please be ready at the pickup location.', ride_id: rideId });
});
res.json({ message: 'Passengers notified successfully.' });
});
});
});
// Pool Host: start ride
app.post('/api/rides/:id/start', (req, res) => {
const rideId = req.params.id;
db.get(`SELECT driver_id, departure_time, status FROM rides WHERE id = ?`, [rideId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Ride not found' });
if (row.driver_id !== req.user.id && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
if (row.status !== 'Scheduled') {
return res.status(400).json({ error: 'Ride is not in Scheduled state' });
}
// Enforce +/- 10 minutes window
const now = new Date();
const current_minutes = now.getHours() * 60 + now.getMinutes();
const [d_hh, d_mm] = row.departure_time.split(':').map(Number);
const departure_minutes = d_hh * 60 + d_mm;
if (Math.abs(departure_minutes - current_minutes) > 10) {
return res.status(400).json({ error: 'You can only start the ride within 10 minutes of the scheduled departure time.' });
}
const today = new Date().toISOString().split('T')[0];
db.run(`INSERT INTO trip_logs (ride_id, date, status) VALUES (?, ?, 'In Progress')
ON CONFLICT(ride_id, date) DO UPDATE SET status = 'In Progress'`, [rideId, today], function(err) {
if (err && err.message.includes('syntax error')) {
db.run(`INSERT INTO trip_logs (ride_id, date, status) VALUES (?, ?, 'In Progress')`, [rideId, today]);
}
// Update the rides table for current day's tracking
db.run(`UPDATE rides SET actual_start_time = CURRENT_TIMESTAMP, delay_reminder_sent = 0 WHERE id = ?`, [rideId]);
db.all(`SELECT passenger_id FROM ride_requests WHERE ride_id = ? AND status = 'Accepted'`, [rideId], (err, passengers) => {
if (!err) {
passengers.forEach(p => {
req.app.get('io').to(`user_${p.passenger_id}`).emit('ride_started', { message: 'Your commute has started. Have a safe journey!', ride_id: rideId });
});
}
res.json({ message: 'Ride started successfully for today.' });
});
});
});
});
// Pool Host: accept request OR Passenger: accept offer
app.post('/api/rides/request/:id/accept', (req, res) => {
const requestId = req.params.id;
db.get(`SELECT rr.*, r.seats_offered, r.status as ride_status, r.driver_id FROM ride_requests rr JOIN rides r ON rr.ride_id = r.id WHERE rr.id = ?`, [requestId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Request not found' });
const isDriver = row.driver_id === req.user.id;
const isPassenger = row.passenger_id === req.user.id;
if (!isDriver && !isPassenger && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to accept this request/offer' });
}
if (row.ride_status !== 'Scheduled') return res.status(400).json({ error: 'Ride is no longer active' });
if (row.seats_offered <= 0) return res.status(400).json({ error: 'No seats available to accept this request' });
db.serialize(() => {
// Update request status
db.run(`UPDATE ride_requests SET status = 'Accepted' WHERE id = ?`, [requestId], function(err) {
if (err) return res.status(500).json({ error: err.message });
// Decrement seats offered
db.run(`UPDATE rides SET seats_offered = seats_offered - 1 WHERE id = ?`, [row.ride_id], function(err) {
if (err) return res.status(500).json({ error: err.message });
if (isDriver) {
req.app.get('io').to(`user_${row.passenger_id}`).emit('ride_accepted', { message: 'Your ride request was accepted!' });
sendPushToUser(row.passenger_id, 'Ride Accepted', 'Your ride request was accepted!');
} else {
req.app.get('io').to(`user_${row.driver_id}`).emit('ride_accepted', { message: 'A passenger accepted your ride offer!' });
sendPushToUser(row.driver_id, 'Ride Accepted', 'A passenger accepted your ride offer!');
}
recalculateMultiStopRoute(row.ride_id, req.app.get('io'));
res.json({ message: 'Request accepted successfully.' });
});
});
});
});
});
// Pool Host: reject request OR Passenger: reject offer
app.post('/api/rides/request/:id/reject', (req, res) => {
const requestId = req.params.id;
db.get(`SELECT rr.*, r.driver_id FROM ride_requests rr JOIN rides r ON rr.ride_id = r.id WHERE rr.id = ?`, [requestId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Request not found' });
const isDriver = row.driver_id === req.user.id;
const isPassenger = row.passenger_id === req.user.id;
if (!isDriver && !isPassenger && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to reject this request/offer' });
}
db.run(`UPDATE ride_requests SET status = 'Rejected' WHERE id = ?`, [requestId], function(err) {
if (err) return res.status(500).json({ error: err.message });
if (isDriver) {
req.app.get('io').to(`user_${row.passenger_id}`).emit('ride_rejected', { message: 'Your ride request was declined.' });
sendPushToUser(row.passenger_id, 'Ride Declined', 'Your ride request was declined.');
} else {
req.app.get('io').to(`user_${row.driver_id}`).emit('ride_rejected', { message: 'A passenger declined your ride offer.' });
sendPushToUser(row.driver_id, 'Ride Declined', 'A passenger declined your ride offer.');
}
recalculateMultiStopRoute(row.ride_id, req.app.get('io'));
res.json({ message: 'Request rejected successfully.' });
});
});
});
// Mark a No-Show
app.post('/api/rides/request/:id/no-show', (req, res) => {
const requestId = req.params.id;
const { reported_by_role } = req.body; // 'Host' or 'Passenger'
db.get(`SELECT rr.*, r.driver_id, r.status as ride_status FROM ride_requests rr JOIN rides r ON rr.ride_id = r.id WHERE rr.id = ?`, [requestId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Request not found' });
let targetUserId = null;
if (reported_by_role === 'Host' && req.user.id === row.driver_id) {
targetUserId = row.passenger_id;
} else if (reported_by_role === 'Passenger' && req.user.id === row.passenger_id) {
targetUserId = row.driver_id;
} else {
return res.status(403).json({ error: 'Unauthorized to report no-show for this ride' });
}
db.serialize(() => {
const newStatus = reported_by_role === 'Host' ? 'Passenger No-Show' : 'Host No-Show';
db.run(`UPDATE ride_requests SET status = ? WHERE id = ?`, [newStatus, requestId], function(err) {
if (err) return res.status(500).json({ error: err.message });
if (reported_by_role === 'Host') {
db.run(`UPDATE rides SET seats_offered = seats_offered + 1 WHERE id = ?`, [row.ride_id], () => {});
}
db.run(`UPDATE users SET no_show_count = no_show_count + 1 WHERE id = ?`, [targetUserId], function(err) {
if (err) return res.status(500).json({ error: err.message });
db.get(`SELECT no_show_count FROM users WHERE id = ?`, [targetUserId], (err, userRow) => {
if (userRow && userRow.no_show_count >= 3) {
const type = `Auto-Escalation: User ${targetUserId} has reached ${userRow.no_show_count} No-Shows.`;
const escalateStmt = db.prepare(`INSERT INTO incidents (reported_by, type, sla_deadline) VALUES (?, ?, datetime('now', '+24 hours'))`);
escalateStmt.run([req.user.id, type], (err2) => {
if (err2) console.error('Failed to escalate incident:', err2);
});
escalateStmt.finalize();
}
res.json({ message: 'No-Show reported successfully.' });
});
});
});
});
});
});
// Passenger: Cancel request
app.post('/api/rides/request/:id/cancel', (req, res) => {
const requestId = req.params.id;
db.get(`SELECT rr.*, r.departure_time, r.driver_id, u.name as passenger_name
FROM ride_requests rr
JOIN rides r ON rr.ride_id = r.id
JOIN users u ON rr.passenger_id = u.id
WHERE rr.id = ?`, [requestId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Request not found' });
if (row.passenger_id !== req.user.id && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to cancel this request' });
}
if (row.status !== 'Pending' && row.status !== 'Accepted') {
return res.status(400).json({ error: 'Only pending or accepted requests can be cancelled.' });
}
// Time-check logic: Block cancellation within 15 mins of departure
const now = new Date();
const current_minutes = now.getHours() * 60 + now.getMinutes();
const [c_hh, c_mm] = row.departure_time.split(':').map(Number);
const departure_minutes = c_hh * 60 + c_mm;
if (departure_minutes - current_minutes >= 0 && departure_minutes - current_minutes <= 15) {
return res.status(403).json({ error: 'Cancellation is blocked within 15 minutes of departure.' });
}
db.serialize(() => {
// Mark request as cancelled
db.run(`UPDATE ride_requests SET status = 'Cancelled' WHERE id = ?`, [requestId], function(err) {
if (err) return res.status(500).json({ error: err.message });
// If it was accepted, give the seat back to the driver
if (row.status === 'Accepted') {
db.run(`UPDATE rides SET seats_offered = seats_offered + 1 WHERE id = ?`, [row.ride_id], function(err) {
if (err) console.error('Failed to increment seats:', err.message);
});
}
// Notify the driver
const msg = `Passenger ${row.passenger_name} has cancelled their request for your ride at ${row.departure_time}.`;
db.run(`INSERT INTO notifications (user_id, message, type) VALUES (?, ?, 'RequestCancelled')`, [row.driver_id, msg]);
res.json({ message: 'Request successfully cancelled.' });
});
});
});
});
// No-Show Reporting
app.post('/api/rides/request/:id/no-show', (req, res) => {
const requestId = req.params.id;
const { reported_by_role } = req.body; // 'Host' or 'Passenger'
db.get(`
SELECT rr.*, r.driver_id, r.departure_time,
u_pass.name as passenger_name, u_pass.id as passenger_id,
u_drv.name as driver_name
FROM ride_requests rr
JOIN rides r ON rr.ride_id = r.id
JOIN users u_pass ON rr.passenger_id = u_pass.id
JOIN users u_drv ON r.driver_id = u_drv.id
WHERE rr.id = ?
`, [requestId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Request not found' });
let offenderId;
let reporterName;
if (reported_by_role === 'Host') {
if (req.user.id !== row.driver_id && req.user.role !== 'Admin') return res.status(403).json({ error: 'Unauthorized' });
offenderId = row.passenger_id;
reporterName = row.driver_name;
} else {
if (req.user.id !== row.passenger_id && req.user.role !== 'Admin') return res.status(403).json({ error: 'Unauthorized' });
offenderId = row.driver_id;
reporterName = row.passenger_name;
}
db.serialize(() => {
// 1. Mark request as No-Show
db.run(`UPDATE ride_requests SET status = 'No-Show' WHERE id = ?`, [requestId]);
// 2. Give seat back if reported by Host
if (reported_by_role === 'Host') {
db.run(`UPDATE rides SET seats_offered = seats_offered + 1 WHERE id = ?`, [row.ride_id]);
}
// 3. Increment penalty points
db.get(`SELECT penalty_points FROM users WHERE id = ?`, [offenderId], (err, offender) => {
if (err || !offender) return res.status(500).json({ error: 'Failed to find offender' });
const newPoints = (offender.penalty_points || 0) + 1;
if (newPoints >= 3) {
// Suspend for 15 days
const suspendedUntil = new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString();
db.run(`UPDATE users SET penalty_points = ?, status = 'Suspended', suspended_until = ? WHERE id = ?`, [newPoints, suspendedUntil, offenderId]);
db.run(`INSERT INTO notifications (user_id, message, type) VALUES (?, ?, 'Suspension')`,
[offenderId, `Your account has been suspended for 15 days due to accumulating 3 No-Show penalties.`]);
} else {
db.run(`UPDATE users SET penalty_points = ? WHERE id = ?`, [newPoints, offenderId]);
db.run(`INSERT INTO notifications (user_id, message, type) VALUES (?, ?, 'Penalty')`,
[offenderId, `You were marked as a No-Show by ${reporterName}. You now have ${newPoints}/3 penalty points.`]);
}
res.json({ message: 'No-Show reported successfully.' });
});
});
});
});
// Get user stats (KPIs)
app.get('/api/users/:id/stats', (req, res) => {
const userId = req.params.id;
if (req.user.id !== parseInt(userId) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
db.get(`SELECT * FROM users WHERE id = ?`, [userId], (err, user) => {
if (err) return res.status(500).json({ error: err.message });
if (!user) return res.status(404).json({ error: 'User not found' });
if (user.role === 'Passenger') {
// Passenger KPIs: Rides Taken, Upcoming Ride, COβ‚‚ Saved, Avg. Cost/Ride (β‚Ή)
db.get(`
SELECT
(
SELECT COUNT(*)
FROM ride_requests rr2
JOIN trip_logs tl2 ON rr2.ride_id = tl2.ride_id
WHERE rr2.passenger_id = ? AND rr2.status = 'Accepted' AND tl2.status = 'Completed'
) as rides_taken,
COALESCE(AVG(r.price_per_seat), 0) as avg_cost
FROM ride_requests rr
JOIN rides r ON rr.ride_id = r.id
WHERE rr.passenger_id = ? AND rr.status = 'Accepted'
`, [userId, userId], (err, stats) => {
if (err) return res.status(500).json({ error: err.message });
db.get(`
SELECT r.*, u.name as driver_name, u.vehicle_no, u.avatar_url,
tl.status as today_status
FROM ride_requests rr
JOIN rides r ON rr.ride_id = r.id
JOIN users u ON r.driver_id = u.id
LEFT JOIN trip_logs tl ON r.id = tl.ride_id AND tl.date = ?
WHERE rr.passenger_id = ? AND rr.status = 'Accepted'
AND (tl.status IS NULL OR tl.status != 'Completed')
ORDER BY r.created_at DESC
LIMIT 1
`, [new Date().toISOString().split('T')[0], userId], (err, upcoming) => {
if (err) return res.status(500).json({ error: err.message });
const ridesTaken = stats.rides_taken || 0;
const co2Saved = (ridesTaken * 15 * 0.21).toFixed(1);
res.json({
role: 'Passenger',
rides_taken: ridesTaken,
avg_cost: parseFloat(stats.avg_cost.toFixed(1)),
co2_saved: parseFloat(co2Saved),
upcoming_ride: upcoming || null,
penalty_points: user.penalty_points || 0
});
});
});
} else {
// Pool Host KPIs: Rides Offered, People Carpooled, Pending Requests, Avg. Earning/Ride (β‚Ή), COβ‚‚ Saved
db.get(`
SELECT COUNT(*) as rides_offered FROM rides WHERE driver_id = ?
`, [userId], (err, offerStats) => {
if (err) return res.status(500).json({ error: err.message });
db.get(`
SELECT
COUNT(DISTINCT CASE WHEN rr.status = 'Accepted' THEN rr.id END) as people_carpooled,
COUNT(DISTINCT CASE WHEN rr.status = 'Pending' THEN rr.id END) as pending_requests,
(
SELECT COALESCE(SUM(r2.price_per_seat), 0)
FROM rides r2
JOIN ride_requests rr2 ON r2.id = rr2.ride_id
JOIN trip_logs tl2 ON r2.id = tl2.ride_id
WHERE r2.driver_id = ? AND rr2.status = 'Accepted' AND tl2.status = 'Completed'
) as total_earnings
FROM rides r
LEFT JOIN ride_requests rr ON r.id = rr.ride_id
WHERE r.driver_id = ?
`, [userId, userId], (err, reqStats) => {
if (err) return res.status(500).json({ error: err.message });
const ridesOffered = offerStats.rides_offered || 0;
const peopleCarpooled = reqStats.people_carpooled || 0;
const avgEarning = ridesOffered > 0 ? (reqStats.total_earnings / ridesOffered) : 0;
const co2Saved = (peopleCarpooled * 15 * 0.21).toFixed(1);
res.json({
role: 'Pool Host',
rides_offered: ridesOffered,
people_carpooled: peopleCarpooled,
pending_requests: reqStats.pending_requests || 0,
avg_earning: parseFloat(avgEarning.toFixed(1)),
co2_saved: parseFloat(co2Saved),
penalty_points: user.penalty_points || 0
});
});
});
}
});
});
// Register a new user
app.post('/api/register', async (req, res) => {
const { name, email, password, role, hasVehiclePass, vehicleNo, vehicleMake, vehicleModel, vehicleCapacity, gender } = req.body;
if (!name || !email || !password || !role) {
return res.status(400).json({ error: 'Name, email, password, and role are required' });
}
// Input Validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({ error: 'Invalid email format' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters long' });
}
if (name.length > 100) {
return res.status(400).json({ error: 'Name is too long' });
}
// Role Validation - explicitly block Admin registration
if (role !== 'Passenger' && role !== 'Pool Host') {
return res.status(403).json({ error: 'Invalid role for public registration' });
}
try {
const hashedPassword = await bcrypt.hash(password, 10);
// Check if email exists
db.get(`SELECT emp_id FROM users WHERE email = ?`, [email], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
// Auto-generate or reuse EMP ID
const emp_id = (row && row.emp_id) ? row.emp_id : `EMP${Math.floor(Math.random() * 90000) + 10000}`;
const stmt = db.prepare(`INSERT INTO users (name, email, password, emp_id, role, has_vehicle_pass, vehicle_no, vehicle_make, vehicle_model, vehicle_capacity, gender) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
stmt.run([name, email, hashedPassword, emp_id, role, hasVehiclePass ? 1 : 0, vehicleNo || null, vehicleMake || null, vehicleModel || null, vehicleCapacity || null, gender || null], function(err) {
if (err) {
if (err.message.includes('UNIQUE constraint failed')) {
return res.status(409).json({ error: `This email is already registered as a ${role}.` });
}
return res.status(500).json({ error: err.message });
}
res.status(201).json({ message: 'User registered successfully', userId: this.lastID });
});
stmt.finalize();
});
} catch (hashErr) {
res.status(500).json({ error: 'Failed to hash password' });
}
});
// Pool Host: Offer a ride
app.post('/api/rides/create', (req, res) => {
const { driver_id, origin, destination, origin_lat, origin_lng, dest_lat, dest_lng, departure_time, recurring_days, seats_offered, price_per_seat, women_only } = req.body;
if (req.user.id !== parseInt(driver_id) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to create ride for this driver' });
}
db.get(`SELECT gender FROM users WHERE id = ?`, [driver_id], async (err, driver) => {
if (err) return res.status(500).json({ error: err.message });
if (women_only && driver.gender !== 'Female') {
return res.status(403).json({ error: 'Only Female drivers can list Women-Only rides.' });
}
let route_polyline = null;
let expected_duration_mins = null;
try {
const osrmRes = await fetch(`http://router.project-osrm.org/route/v1/driving/${origin_lng},${origin_lat};${dest_lng},${dest_lat}?geometries=geojson`);
if (osrmRes.ok) {
const osrmData = await osrmRes.json();
if (osrmData.routes && osrmData.routes.length > 0) {
route_polyline = JSON.stringify(osrmData.routes[0].geometry.coordinates);
expected_duration_mins = Math.ceil(osrmData.routes[0].duration / 60);
}
}
} catch (fetchErr) {
console.error("Failed to fetch route polyline:", fetchErr);
}
const stmt = db.prepare(`INSERT INTO rides (driver_id, origin, destination, origin_lat, origin_lng, dest_lat, dest_lng, route_polyline, expected_duration_mins, departure_time, recurring_days, seats_offered, original_seats, price_per_seat, women_only) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const finalRecurringDays = recurring_days || 'Mon,Tue,Wed,Thu,Fri';
stmt.run([driver_id, origin, destination, origin_lat, origin_lng, dest_lat, dest_lng, route_polyline, expected_duration_mins, departure_time, finalRecurringDays, seats_offered, seats_offered, price_per_seat, women_only ? 1 : 0], function(err) {
if (err) return res.status(500).json({ error: err.message });
res.status(201).json({ message: 'Ride created successfully', rideId: this.lastID });
});
stmt.finalize();
});
});
// Passenger: Match rides
app.post('/api/rides/match', (req, res) => {
const { passenger_id, origin_lat, origin_lng, dest_lat, dest_lng, departure_time } = req.body;
if (passenger_id && req.user.id !== parseInt(passenger_id) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
// 1. Convert departure_time (HH:MM string) to minutes since midnight for comparison
const [pass_hh, pass_mm] = departure_time.split(':').map(Number);
const pass_total_minutes = pass_hh * 60 + pass_mm;
// Pre-flight check: Ensure this passenger doesn't already have a ride listed as a driver +/- 30 mins
if (passenger_id) {
db.get(`SELECT emp_id FROM users WHERE id = ?`, [passenger_id], (err, user) => {
if (err) return res.status(500).json({ error: err.message });
if (user && user.emp_id) {
db.all(`
SELECT r.departure_time
FROM rides r
JOIN users u ON r.driver_id = u.id
WHERE u.emp_id = ? AND r.status = 'Scheduled'
`, [user.emp_id], (err, driverRides) => {
if (err) return res.status(500).json({ error: err.message });
for (let dride of driverRides) {
const [d_hh, d_mm] = dride.departure_time.split(':').map(Number);
const d_total = d_hh * 60 + d_mm;
if (Math.abs(d_total - pass_total_minutes) <= 30) {
return res.status(403).json({ error: 'You cannot search for a ride because you already have a commute listed as a Pool Host within 30 minutes of this time.' });
}
}
proceedWithMatching();
});
} else {
proceedWithMatching();
}
});
} else {
proceedWithMatching();
}
function proceedWithMatching() {
db.get(`SELECT gender FROM users WHERE id = ?`, [passenger_id], (err, passUser) => {
const isFemalePassenger = passUser && passUser.gender === 'Female';
const wantsWomenOnly = req.body.women_only && isFemalePassenger;
// 2. Fetch all Scheduled rides with available seats that haven't been logged today
const todayDate = new Date().toISOString().split('T')[0];
const currentDay = new Date().toLocaleDateString('en-US', { weekday: 'short' });
const verifiedOnly = req.body.verified_only === true;
let queryStr = `
SELECT r.*, u.name as driver_name, u.avatar_url as driver_avatar_url, u.vehicle_make, u.vehicle_model, u.vehicle_no, u.gender as driver_gender, u.has_vehicle_pass as driver_has_vehicle_pass
FROM rides r
JOIN users u ON r.driver_id = u.id
WHERE r.status = 'Scheduled' AND r.seats_offered > 0
AND NOT EXISTS (
SELECT 1 FROM trip_logs tl
WHERE tl.ride_id = r.id AND tl.date = ?
)
`;
if (verifiedOnly) {
queryStr += ` AND u.has_vehicle_pass = 1`;
}
db.all(queryStr, [todayDate], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
const doMatching = (threshold) => {
const resultMatches = [];
rows.forEach(ride => {
if (ride.recurring_days && !ride.recurring_days.includes(currentDay)) return;
// Enforce Women-Only rule
if (ride.women_only && !isFemalePassenger) return;
if (wantsWomenOnly && ride.driver_gender !== 'Female') return;
// Check time constraint (+/- 15 mins)
const [drv_hh, drv_mm] = ride.departure_time.split(':').map(Number);
const drv_total_minutes = drv_hh * 60 + drv_mm;
const timeDiff = Math.abs(drv_total_minutes - pass_total_minutes);
if (timeDiff > 15) return;
let distOrigin, distDest;
if (ride.route_polyline) {
try {
const coords = JSON.parse(ride.route_polyline);
// coords is array of [lng, lat]
let minOrigDist = Infinity;
let origIdx = -1;
for (let i = 0; i < coords.length; i++) {
const [lng, lat] = coords[i];
const dist = getDistanceFromLatLonInKm(origin_lat, origin_lng, lat, lng);
if (dist < minOrigDist) {
minOrigDist = dist;
origIdx = i;
}
}
let minDestDist = Infinity;
let destIdx = -1;
for (let i = origIdx; i < coords.length; i++) { // enforce direction
const [lng, lat] = coords[i];
const dist = getDistanceFromLatLonInKm(dest_lat, dest_lng, lat, lng);
if (dist < minDestDist) {
minDestDist = dist;
destIdx = i;
}
}
distOrigin = minOrigDist;
distDest = minDestDist;
} catch (e) {
console.error("Polyline parse error", e);
distOrigin = getDistanceFromLatLonInKm(origin_lat, origin_lng, ride.origin_lat, ride.origin_lng);
distDest = getDistanceFromLatLonInKm(dest_lat, dest_lng, ride.dest_lat, ride.dest_lng);
}
} else {
// Fallback to simple point-to-point if no polyline
distOrigin = getDistanceFromLatLonInKm(origin_lat, origin_lng, ride.origin_lat, ride.origin_lng);
distDest = getDistanceFromLatLonInKm(dest_lat, dest_lng, ride.dest_lat, ride.dest_lng);
}
if (distOrigin <= threshold && distDest <= threshold) {
resultMatches.push({
...ride,
route_polyline: ride.route_polyline,
dist_origin_km: parseFloat(distOrigin.toFixed(2)),
dist_dest_km: parseFloat(distDest.toFixed(2)),
total_dist_km: parseFloat((distOrigin + distDest).toFixed(2)),
time_deviation_mins: drv_total_minutes - pass_total_minutes
});
}
});
resultMatches.sort((a, b) => a.total_dist_km - b.total_dist_km);
return resultMatches;
};
let matches = doMatching(3);
let expandedRadius = false;
if (matches.length === 0) {
matches = doMatching(5); // increase to 5km
if (matches.length > 0) {
expandedRadius = true;
}
}
res.json({ matches, expandedRadius });
});
});
}
});
// Passenger: Post a Commute Need
app.post('/api/passenger-listings/create', (req, res) => {
const { passenger_id, origin, destination, origin_lat, origin_lng, dest_lat, dest_lng, departure_time, recurring_days } = req.body;
if (req.user.id !== parseInt(passenger_id) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to list for this passenger' });
}
const stmt = db.prepare(`INSERT INTO passenger_listings (passenger_id, origin, destination, origin_lat, origin_lng, dest_lat, dest_lng, departure_time, recurring_days) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
const finalRecurringDays = recurring_days || 'Mon,Tue,Wed,Thu,Fri';
stmt.run([passenger_id, origin, destination, origin_lat, origin_lng, dest_lat, dest_lng, departure_time, finalRecurringDays], function(err) {
if (err) return res.status(500).json({ error: err.message });
res.status(201).json({ message: 'Commute need posted successfully', listingId: this.lastID });
});
stmt.finalize();
});
// Host: Browse passenger listings (Reverse Matching)
app.get('/api/passenger-listings/match/:driverId', (req, res) => {
const driverId = req.params.driverId;
if (req.user.id !== parseInt(driverId) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized' });
}
db.get(`SELECT id, route_polyline, origin_lat, origin_lng, dest_lat, dest_lng, departure_time FROM rides WHERE driver_id = ? AND status = 'Scheduled' ORDER BY id DESC LIMIT 1`, [driverId], (err, ride) => {
if (err) return res.status(500).json({ error: err.message });
if (!ride) return res.json({ matches: [], error: 'You need an active scheduled ride to find passengers.' });
const verifiedOnly = req.query.verified_only === 'true';
let queryStr = `
SELECT pl.*, u.name as passenger_name, u.gender as passenger_gender, u.avatar_url as passenger_avatar_url, u.has_vehicle_pass
FROM passenger_listings pl
JOIN users u ON pl.passenger_id = u.id
WHERE pl.status = 'Active'
`;
if (verifiedOnly) {
queryStr += ` AND u.has_vehicle_pass = 1`;
}
db.all(queryStr, [], (err, listings) => {
if (err) return res.status(500).json({ error: err.message });
const matches = [];
const currentDay = new Date().toLocaleDateString('en-US', { weekday: 'short' });
listings.forEach(listing => {
if (listing.recurring_days && !listing.recurring_days.includes(currentDay)) return;
let distOrigin, distDest;
if (ride.route_polyline) {
try {
const coords = JSON.parse(ride.route_polyline);
let minOrigDist = Infinity;
let origIdx = -1;
for (let i = 0; i < coords.length; i++) {
const [lng, lat] = coords[i];
const dist = getDistanceFromLatLonInKm(listing.origin_lat, listing.origin_lng, lat, lng);
if (dist < minOrigDist) { minOrigDist = dist; origIdx = i; }
}
let minDestDist = Infinity;
let destIdx = -1;
for (let i = origIdx; i < coords.length; i++) {
const [lng, lat] = coords[i];
const dist = getDistanceFromLatLonInKm(listing.dest_lat, listing.dest_lng, lat, lng);
if (dist < minDestDist) { minDestDist = dist; destIdx = i; }
}
distOrigin = minOrigDist;
distDest = minDestDist;
} catch (e) {
distOrigin = getDistanceFromLatLonInKm(listing.origin_lat, listing.origin_lng, ride.origin_lat, ride.origin_lng);
distDest = getDistanceFromLatLonInKm(listing.dest_lat, listing.dest_lng, ride.dest_lat, ride.dest_lng);
}
} else {
distOrigin = getDistanceFromLatLonInKm(listing.origin_lat, listing.origin_lng, ride.origin_lat, ride.origin_lng);
distDest = getDistanceFromLatLonInKm(listing.dest_lat, listing.dest_lng, ride.dest_lat, ride.dest_lng);
}
if (distOrigin <= 5 && distDest <= 5) {
matches.push({
...listing,
ride_id: ride.id,
dist_origin_km: parseFloat(distOrigin.toFixed(2)),
dist_dest_km: parseFloat(distDest.toFixed(2)),
total_dist_km: parseFloat((distOrigin + distDest).toFixed(2))
});
}
});
matches.sort((a, b) => a.total_dist_km - b.total_dist_km);
res.json({ matches });
});
});
});
// Create an Incident (SOS)
app.post('/api/incidents/create', (req, res) => {
const { reported_by, type } = req.body;
if (!type || type.length > 200) {
return res.status(400).json({ error: 'Valid incident type is required (max 200 chars)' });
}
// Set SLA deadline to 15 minutes from now
const slaDeadline = new Date(Date.now() + 15 * 60000).toISOString();
const stmt = db.prepare(`INSERT INTO incidents (reported_by, type, sla_deadline) VALUES (?, ?, ?)`);
stmt.run([reported_by, type, slaDeadline], function(err) {
if (err) return res.status(500).json({ error: err.message });
req.app.get('io').to('admin_room').emit('sos_alert', { message: `New SOS Alert triggered: ${type}` });
db.all(`SELECT id FROM users WHERE role = 'Admin'`, [], (err, admins) => {
if (!err) admins.forEach(admin => sendPushToUser(admin.id, 'SOS Alert', `New SOS Alert: ${type}`));
});
res.status(201).json({ message: 'SOS Alert triggered successfully', incidentId: this.lastID });
});
stmt.finalize();
});
// Get all incidents
app.get('/api/incidents', (req, res) => {
if (req.user.role !== 'Admin') return res.status(403).json({ error: 'Admin only' });
db.all(`
SELECT i.*, u.name as reporter_name, u.role as reporter_role, u.vehicle_no
FROM incidents i
JOIN users u ON i.reported_by = u.id
ORDER BY i.created_at DESC
`, [], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});
// Resolve an incident
app.post('/api/incidents/resolve', (req, res) => {
if (req.user.role !== 'Admin') return res.status(403).json({ error: 'Admin only' });
const { incident_id } = req.body;
db.run(`UPDATE incidents SET status = 'Resolved' WHERE id = ?`, [incident_id], function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ message: 'Incident resolved' });
});
});
// Escalate an incident
app.post('/api/incidents/escalate', (req, res) => {
if (req.user.role !== 'Admin') return res.status(403).json({ error: 'Admin only' });
const { incident_id } = req.body;
db.run(`UPDATE incidents SET status = 'Escalated' WHERE id = ?`, [incident_id], function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ message: 'Incident escalated to ERT' });
});
});
// Get user notifications
app.get('/api/users/:id/notifications', (req, res) => {
const userId = req.params.id;
if (req.user.id !== parseInt(userId) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to view these notifications' });
}
db.all(`SELECT * FROM notifications WHERE user_id = ? AND is_read = 0 ORDER BY created_at DESC`, [userId], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});
// Mark notification as read
app.post('/api/notifications/:id/read', (req, res) => {
const notifId = req.params.id;
db.get(`SELECT user_id FROM notifications WHERE id = ?`, [notifId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
if (!row) return res.status(404).json({ error: 'Notification not found' });
if (row.user_id !== req.user.id && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to mark this notification as read' });
}
db.run(`UPDATE notifications SET is_read = 1 WHERE id = ?`, [notifId], function(err) {
if (err) return res.status(500).json({ error: err.message });
res.json({ message: 'Notification marked as read' });
});
});
});
// Get all trips (Rides)
app.get('/api/trips', (req, res) => {
if (req.user.role !== 'Admin') return res.status(403).json({ error: 'Admin only' });
db.all(`
SELECT r.*, u.name as driver_name, u.vehicle_no
FROM rides r
JOIN users u ON r.driver_id = u.id
ORDER BY r.created_at DESC
`, [], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});
// Admin: Reset penalty points
app.post('/api/users/:id/reset-penalty', (req, res) => {
const userId = req.params.id;
if (req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Only Admins can reset penalty points.' });
}
db.run(`UPDATE users SET penalty_points = 0 WHERE id = ?`, [userId], function(err) {
if (err) return res.status(500).json({ error: err.message });
if (this.changes === 0) return res.status(404).json({ error: 'User not found' });
// Also log notification
db.run(`INSERT INTO notifications (user_id, message, type) VALUES (?, ?, 'Penalty')`,
[userId, `Your penalty points have been forgiven by an Admin.`]);
res.json({ message: 'Penalty points reset successfully', status: 'Active' });
});
});
// ─── AI Chat Endpoint ─────────────────────────────────────────────────────────
app.post('/api/chat', async (req, res) => {
if (!activeAI) {
return res.status(500).json({
error: AI_PROVIDER === 'nvidia'
? 'NVIDIA API key not configured. Add NVIDIA_API_KEY to server/.env (get key at https://build.nvidia.com)'
: 'AI provider not configured. Check server/.env'
});
}
try {
const { message, context, history } = req.body;
// Build the user's final message with injected context
const userContent = context
? `[System Note: The current user is a ${context.userRole}. Their live stats/KPIs are: ${JSON.stringify(context.stats)}]\n\nUser Query: ${message}`
: message;
// ── NVIDIA NIM path (OpenAI-compatible) ────────────────────────────────────
if (activeAI === 'nvidia') {
// Convert Gemini-style history { role:'model', parts:[{text}] }
// β†’ OpenAI-style { role:'assistant'|'user', content:string }
const openaiHistory = (history || []).map(m => ({
role: m.role === 'model' ? 'assistant' : 'user',
content: Array.isArray(m.parts) ? m.parts.map(p => p.text || '').join('') : (m.content || '')
}));
const messages = [
{ role: 'system', content: systemPrompt },
...openaiHistory,
{ role: 'user', content: userContent }
];
const completion = await nvidiaClient.chat.completions.create({
model: NVIDIA_MODEL,
messages,
tools: CARPOOL_TOOLS,
tool_choice: 'auto',
temperature: 0.6,
max_tokens: 1024,
});
const choice = completion.choices[0];
const assistantMsg = choice.message;
// Check for tool calls
if (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) {
const call = assistantMsg.tool_calls[0];
let params;
try {
params = typeof call.function.arguments === 'string'
? JSON.parse(call.function.arguments)
: call.function.arguments;
} catch (e) {
params = {};
}
return res.json({
type: 'action',
action: call.function.name,
params,
text: `I've prepared a draft for your ${call.function.name === 'draft_booking' ? 'ride request' : 'commute listing'}. Please review and confirm the details below.`
});
}
// Plain text response
return res.json({
type: 'text',
text: assistantMsg.content || ''
});
}
// ── Gemini fallback path ───────────────────────────────────────────────────
if (activeAI === 'gemini') {
const chat = geminiModel.startChat({ history: history || [] });
const result = await chat.sendMessage(userContent);
const response = result.response;
const functionCalls = response.functionCalls();
if (functionCalls && functionCalls.length > 0) {
const call = functionCalls[0];
return res.json({
type: 'action',
action: call.name,
params: call.args,
text: `I've prepared a draft for your ${call.name === 'draft_booking' ? 'ride request' : 'commute listing'}. Please confirm the details.`
});
}
return res.json({ type: 'text', text: response.text() });
}
} catch (err) {
console.error('[AI] Chat error:', err.message || err);
// Return a helpful message so the UI doesn't break
const isAuthError = err?.status === 401 || (err?.message || '').includes('401');
const isQuotaError = err?.status === 429 || (err?.message || '').includes('429');
res.status(500).json({
error: isAuthError
? 'Invalid NVIDIA API key. Please check NVIDIA_API_KEY in server/.env'
: isQuotaError
? 'NVIDIA API rate limit reached. Please wait a moment and try again.'
: 'AI service temporarily unavailable. Please try again.'
});
}
});
// Chat Endpoints
app.get('/api/messages/:request_id', (req, res) => {
const requestId = req.params.request_id;
// Verify request is accepted and user is involved
db.get(`
SELECT rr.status, rr.passenger_id, r.driver_id
FROM ride_requests rr
JOIN rides r ON rr.ride_id = r.id
WHERE rr.id = ?
`, [requestId], (err, request) => {
if (err) return res.status(500).json({ error: err.message });
if (!request || request.status !== 'Accepted') {
return res.status(403).json({ error: 'Chat is only available for accepted rides.' });
}
if (req.user.id !== request.passenger_id && req.user.id !== request.driver_id && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to view these messages' });
}
db.all(`
SELECT m.*, u.name as sender_name
FROM messages m
JOIN users u ON m.sender_id = u.id
WHERE m.request_id = ?
ORDER BY m.created_at ASC
`, [requestId], (err, rows) => {
if (err) return res.status(500).json({ error: err.message });
res.json(rows);
});
});
});
app.post('/api/messages/:request_id', (req, res) => {
const requestId = req.params.request_id;
const { sender_id, content } = req.body;
if (!sender_id || !content) return res.status(400).json({ error: 'sender_id and content required' });
if (req.user.id !== parseInt(sender_id) && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to send message as this user' });
}
// Verify request is accepted and user is involved
db.get(`
SELECT rr.status, rr.passenger_id, r.driver_id
FROM ride_requests rr
JOIN rides r ON rr.ride_id = r.id
WHERE rr.id = ?
`, [requestId], (err, request) => {
if (err) return res.status(500).json({ error: err.message });
if (!request || request.status !== 'Accepted') {
return res.status(403).json({ error: 'Chat is only available for accepted rides.' });
}
if (req.user.id !== request.passenger_id && req.user.id !== request.driver_id && req.user.role !== 'Admin') {
return res.status(403).json({ error: 'Unauthorized to participate in this chat' });
}
const stmt = db.prepare(`INSERT INTO messages (request_id, sender_id, content) VALUES (?, ?, ?)`);
stmt.run([requestId, sender_id, content], function(err) {
if (err) return res.status(500).json({ error: err.message });
res.status(201).json({ message: 'Message sent successfully', messageId: this.lastID });
});
stmt.finalize();
});
});
// --- Ratings API ---
app.post('/api/ratings', (req, res) => {
const { trip_id, ratee_id, stars, tags, comment } = req.body;
const rater_id = req.user.id;
if (!stars) {
return res.status(400).json({ error: 'stars are required.' });
}
const finalTripId = trip_id || 0;
const finalRateeId = ratee_id || 0;
// 1. Insert rating
const stmt = db.prepare(`INSERT INTO ratings (trip_id, rater_id, ratee_id, stars, tags, comment) VALUES (?, ?, ?, ?, ?, ?)`);
stmt.run([finalTripId, rater_id, finalRateeId, stars, tags || '', comment || ''], function(err) {
if (err) return res.status(500).json({ error: err.message });
// 2. Auto-escalate to Incident Queue if negative safety tags present
const tagsLower = (tags || '').toLowerCase();
if (tagsLower.includes('harassment') || tagsLower.includes('unsafe driving') || tagsLower.includes('rash driving')) {
const type = `Auto-Escalation: Safety tag triggered by User ${rater_id} against User ${ratee_id}`;
const escalateStmt = db.prepare(`INSERT INTO incidents (reported_by, type, sla_deadline) VALUES (?, ?, datetime('now', '+24 hours'))`);
escalateStmt.run([rater_id, type], (err2) => {
if (err2) console.error('Failed to escalate incident:', err2);
});
escalateStmt.finalize();
}
res.status(201).json({ message: 'Rating submitted successfully.' });
});
stmt.finalize();
});
app.get('/api/users/:id/rating', (req, res) => {
const userId = req.params.id;
db.get(`SELECT AVG(stars) as avg_rating, COUNT(id) as total_ratings FROM ratings WHERE ratee_id = ?`, [userId], (err, row) => {
if (err) return res.status(500).json({ error: err.message });
res.json({
avg_rating: row.avg_rating ? parseFloat(row.avg_rating).toFixed(1) : null,
total_ratings: row.total_ratings || 0
});
});
});
// Background task for ETA delay monitoring
setInterval(() => {
db.all(`
SELECT r.id, r.driver_id, r.expected_duration_mins, r.actual_start_time, r.delay_reminder_sent
FROM rides r
JOIN trip_logs tl ON r.id = tl.ride_id AND tl.date = ?
WHERE tl.status = 'In Progress' AND r.actual_start_time IS NOT NULL AND r.expected_duration_mins IS NOT NULL
`, [new Date().toISOString().split('T')[0]], (err, rides) => {
if (err || !rides) return;
const now = new Date();
rides.forEach(ride => {
const startTime = new Date(ride.actual_start_time);
const elapsedMins = (now - startTime) / 60000;
if (elapsedMins > ride.expected_duration_mins + 30 && ride.delay_reminder_sent === 0) {
// Step 1: Push reminder to host
io.to(`user_${ride.driver_id}`).emit('eta_alert', {
message: `Your ride has exceeded the expected ETA by over 30 mins! Please remember to complete the ride if you have dropped off passengers, or contact support if delayed.`
});
db.run(`UPDATE rides SET delay_reminder_sent = 1 WHERE id = ?`, [ride.id]);
} else if (elapsedMins > ride.expected_duration_mins + 35 && ride.delay_reminder_sent === 1) {
// Step 2: Escalate to Admin SOS
const incidentType = `Auto-Escalation: Ride #${ride.id} exceeded ETA by >35 mins without completion.`;
db.run(`INSERT INTO incidents (reported_by, type, sla_deadline) VALUES (?, ?, datetime('now', '+15 minutes'))`,
[ride.driver_id, incidentType], function(err2) {
if (!err2) {
io.to('admin_room').emit('sos_alert', { message: `Urgent: ${incidentType}` });
db.all(`SELECT id FROM users WHERE role = 'Admin'`, [], (err, admins) => {
if (!err) admins.forEach(admin => sendPushToUser(admin.id, 'SOS Alert', `Urgent: ${incidentType}`));
});
db.run(`UPDATE rides SET delay_reminder_sent = 2 WHERE id = ?`, [ride.id]);
}
});
}
});
});
}, 60000); // Check every minute
// Serve static frontend files
app.use(express.static(path.join(__dirname, '../dist')));
// Catch-all to serve index.html for React Router
app.use((req, res) => {
res.sendFile(path.join(__dirname, '../dist', 'index.html'));
});
server.listen(port, () => {
console.log(`Server & WebSocket running at http://localhost:${port}`);
});