Spaces:
Sleeping
Sleeping
Download server/server.js from Shivam4096/Carpool: direct link, hf CLI and curl.
- Browser
- Download file 95.3 kB
-
https://huggingface.co/spaces/Shivam4096/Carpool/resolve/main/server/server.js
- Command line
-
hf download hf://spaces/Shivam4096/Carpool/server/server.js
-
curl -L -o server.js https://huggingface.co/spaces/Shivam4096/Carpool/resolve/main/server/server.js
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}`); | |
| }); | |