import express from 'express' import cors from 'cors' import https from 'https' import http from 'http' import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import { Parser } from 'json2csv' import { v4 as uuidv4 } from 'uuid' import mysql from 'mysql2/promise' import dotenv from 'dotenv' import bcrypt from 'bcrypt' import jwt from 'jsonwebtoken' import { point, polygon, booleanPointInPolygon, pointToLineDistance } from '@turf/turf' async function validateDeviceForUser(userId, deviceUuid, db) { try { // Step 1: Get user's current registered device UUID from workers table const [userRows] = await db.execute( 'SELECT device_uuid, role, username FROM workers WHERE id = ?', [userId] ) if (userRows.length === 0) { return { valid: false, message: 'User not found' } } const user = userRows[0] const registeredDeviceUuid = user.device_uuid // Step 2: If no device is registered for this user (NULL device_uuid) if (!registeredDeviceUuid) { // Check if this device UUID is already registered to another user const [otherUserRows] = await db.execute( 'SELECT id, username FROM workers WHERE device_uuid = ? AND id != ?', [deviceUuid, userId] ) if (otherUserRows.length > 0) { // Log security alert for device conflict await logSecurityAlert(userId, 'device_conflict', { attempted_device_uuid: deviceUuid, conflicting_user: otherUserRows[0].username, message: 'Attempted to register device already assigned to another user' }, db) return { valid: false, message: 'Device is already registered to another account' } } // Step 3: Auto-register this device for the user (initial setup) await db.execute( 'UPDATE workers SET device_uuid = ? WHERE id = ?', [deviceUuid, userId] ) console.log(`Auto-registered device ${deviceUuid} for user ${userId} (${user.username}) - initial setup`) return { valid: true, message: 'Device registered successfully for first-time login' } } // Step 4: User has a registered device - check if current device matches if (registeredDeviceUuid === deviceUuid) { // Device UUID matches - allow login return { valid: true, message: 'Device validated successfully' } } // Step 5: Device UUID mismatch - log security alert and reject await logSecurityAlert(userId, 'unauthorized_device_attempt', { registered_device_uuid: registeredDeviceUuid, attempted_device_uuid: deviceUuid, username: user.username, message: 'Login attempt from unauthorized device' }, db) return { valid: false, message: 'This device is not authorized for your account. Please use your registered device.' } } catch (error) { console.error('Device validation error:', error) return { valid: false, message: 'Device validation failed' } } } // Helper function to log security alerts for device validation issues async function logSecurityAlert(userId, alertType, alertData, db) { try { await db.execute( 'INSERT INTO security_alerts (user_id, alert_type, alert_data, severity, created_at) VALUES (?, ?, ?, ?, NOW())', [userId, alertType, JSON.stringify(alertData), 'medium'] ) console.log(`Security alert logged: ${alertType} for user ${userId}`) } catch (error) { console.error('Failed to log security alert:', error) } } // Helper function to register a new device for user (simplified for workers table) async function registerDeviceForUser(userId, deviceUuid, db) { try { // Check if device is already registered to another user const [otherUserRows] = await db.execute( 'SELECT id, username FROM workers WHERE device_uuid = ? AND id != ?', [deviceUuid, userId] ) if (otherUserRows.length > 0) { // Log security alert for device conflict await logSecurityAlert(userId, 'device_registration_conflict', { attempted_device_uuid: deviceUuid, conflicting_user: otherUserRows[0].username, message: 'Attempted to register device already assigned to another user' }, db) return { success: false, message: 'Device is already registered to another account' } } // Register device by updating workers table await db.execute( 'UPDATE workers SET device_uuid = ? WHERE id = ?', [deviceUuid, userId] ) console.log(`Device ${deviceUuid} registered for user ${userId}`) return { success: true, message: 'Device registered successfully' } } catch (error) { console.error('Device registration error:', error) return { success: false, message: 'Database error during device registration' } } } // Main function to start the server async function startServer() { dotenv.config({ path: path.join(path.dirname(fileURLToPath(import.meta.url)), '.env') }) const app = express() // --- Database Connection --- const db = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, port: process.env.DB_PORT, waitForConnections: true, connectionLimit: 10, queueLimit: 0, }) try { const connection = await db.getConnection() console.log('Database connected successfully!') connection.release() } catch (error) { console.error('!!! DATABASE CONNECTION FAILED !!!') console.error('Error:', error.message) process.exit(1) } // Define the geofence polygon by calling the 'polygon' function directly // const geofence = polygon([ // [ // [101.80827335908509, 2.8350045747358337], // [101.80822799653066, 2.8340134829130363], // [101.80827902940462, 2.8335264317641418], // [101.80941309326164, 2.8332772427247335], // [101.81144873788423, 2.834596811345506], // [101.81166988033686, 2.8345911479647157], // [101.81199875885511, 2.83593336858695], // [101.80827335908509, 2.8350045747358337], // ], // ]) const geofence = polygon([ [ [113.35311466293217, 23.161344441258407], [113.28591534444001, 23.161344441258407], [113.28591534444001, 23.091366234233973], [113.35311466293217, 23.091366234233973], [113.35311466293217, 23.161344441258407] ] ]) // Enhanced CORS configuration for HTTPS and mobile development const corsOptions = { origin: function (origin, callback) { // Allow requests with no origin (mobile apps, Postman, etc.) if (!origin) return callback(null, true) // Define allowed origins const allowedOrigins = [ 'http://localhost:5173', 'https://localhost:5173', 'capacitor://localhost', 'ionic://localhost', 'http://localhost', 'https://localhost' ] // Add environment-specific origins if (process.env.CORS_ORIGIN) { const envOrigins = process.env.CORS_ORIGIN.split(',') allowedOrigins.push(...envOrigins) } // Add local IP origins for mobile testing if (process.env.SERVER_IP) { const serverIP = process.env.SERVER_IP allowedOrigins.push( `http://${serverIP}:5173`, `https://${serverIP}:5173`, `http://${serverIP}:3000`, `https://${serverIP}:3443` ) } // Check if origin is allowed if (allowedOrigins.indexOf(origin) !== -1 || origin.startsWith('capacitor://') || origin.startsWith('ionic://')) { callback(null, true) } else { console.log('CORS blocked origin:', origin) callback(null, true) // Allow all origins in development } }, credentials: true, methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization', 'ngrok-skip-browser-warning'], exposedHeaders: ['Content-Range', 'X-Content-Range'] } app.use(cors(corsOptions)) app.use(express.json()) // --- API Endpoints --- // Auth Endpoint with Device UUID validation app.post('/api/auth/login', async (req, res) => { try { const { username, password, deviceUuid } = req.body const [rows] = await db.execute( 'SELECT id, role, password_hash FROM workers WHERE username = ?', [username], ) if (rows.length > 0) { const user = rows[0] const passwordMatch = await bcrypt.compare(password, user.password_hash) if (passwordMatch) { // Check device UUID validation if provided, but skip for managers if (deviceUuid && user.role !== 'manager') { const deviceValidation = await validateDeviceForUser(user.id, deviceUuid, db) if (!deviceValidation.valid) { return res.status(403).json({ message: 'Device not authorized for this account', deviceError: deviceValidation.message }) } } const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h', }) res.json({ message: 'Login successful', token }) } else { res.status(401).json({ message: 'Invalid credentials' }) } } else { res.status(401).json({ message: 'Invalid credentials' }) } } catch (error) { console.error('Login error:', error) res.status(500).json({ message: 'Database error during login.' }) } }) // Middleware to verify JWT const authenticateJWT = (req, res, next) => { const authHeader = req.headers.authorization if (authHeader) { const token = authHeader.split(' ')[1] jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) { return res.status(403).json({ message: 'Forbidden' }) } req.user = user next() }) } else { res.status(401).json({ message: 'Unauthorized' }) } } // Worker Clock In/Out Endpoint - Optimized Version app.post('/api/clock', authenticateJWT, async (req, res) => { try { const { userId, eventType, qrCodeValue, latitude, longitude, notes } = req.body; const connection = await db.getConnection(); // Get connection from pool first try { // Start transaction await connection.beginTransaction(); // Bypass checks for special cases if (qrCodeValue !== 'FORCE_CLOCK_OUT') { // Parallelize geofence and QR code checks const [geofenceCheck, qrCheck] = await Promise.all([ booleanPointInPolygon(point([longitude, latitude]), geofence), connection.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [qrCodeValue]) ]); if (!geofenceCheck) { const distance = pointToLineDistance(point([longitude, latitude]), geofence.geometry.coordinates[0], { units: 'meters' }); const notes = `Clock-in outside of the zone: ${distance.toFixed(2)} meters.`; await connection.execute( 'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?)', [userId, 'failed', new Date(), qrCodeValue, latitude, longitude, notes] ); await connection.commit(); return res.status(403).json({ message: `You are not within the allowed work area.` }); } if (qrCheck[0].length === 0) { await connection.rollback(); return res.status(400).json({ message: 'Invalid QR Code scanned.' }); } if (!qrCheck[0][0].is_active) { await connection.rollback(); return res.status(400).json({ message: 'This QR Code has expired and is no longer active.' }); } } // Check last event const [lastEvent] = await connection.execute( 'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [userId] ); if (qrCodeValue === 'FORCE_CLOCK_OUT') { // If it's a forced clock-out, log it as a failed event regardless of previous state await connection.execute( 'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?)', [userId, 'failed', new Date(), qrCodeValue, latitude, longitude, `FAKE GPS APP Detected.`] ); await connection.commit(); return res.status(200).json({ message: 'Forced clock-out attempt was logged.' }); } if (lastEvent.length > 0 && lastEvent[0].event_type === eventType) { await connection.rollback(); return res.status(400).json({ message: `You are already clocked ${eventType === 'clock_in' ? 'in' : 'out'}.` }); } // Insert new record const timestamp = new Date(); await connection.execute( 'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?)', [userId, eventType, timestamp, qrCodeValue || null, latitude || null, longitude || null, notes || null] ); await connection.commit(); res.status(201).json({ message: 'Clock event recorded successfully' }); } catch (err) { await connection.rollback(); throw err; } finally { connection.release(); } } catch (error) { console.error('Clock event error:', error); res.status(500).json({ message: 'Database error during clock event.' }); } }) // Fetch worker details endpoint app.get('/api/workers/:id', authenticateJWT, async (req, res) => { try { const { id } = req.params const [rows] = await db.execute( 'SELECT full_name FROM workers WHERE id = ? AND role = "worker"', [id], ) if (rows.length > 0) { res.json({ full_name: rows[0].full_name }) } else { res.status(404).json({ message: 'Worker not found.' }) } } catch (error) { console.error('Get worker details error:', error) res.status(500).json({ message: 'Database error fetching worker details.' }) } }) // Worker Status Endpoint app.get('/api/worker/status/:userId', authenticateJWT, async (req, res) => { try { const { userId } = req.params const [rows] = await db.execute( 'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [userId], ) if (rows.length > 0) { res.json({ eventType: rows[0].event_type }) } else { res.json({ eventType: 'clock_out' }) // Default to clocked out } } catch (error) { console.error('Worker status error:', error) res.status(500).json({ message: 'Database error fetching status.' }) } }) // Worker History Endpoint app.get('/api/worker/clock-history/:userId', authenticateJWT, async (req, res) => { try { const { userId } = req.params // MODIFIED: Use LEFT JOIN and COALESCE to handle manual entries const [rows] = await db.execute( `SELECT cr.id, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName FROM clock_records cr LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id WHERE cr.worker_id = ? ORDER BY cr.timestamp DESC`, [userId], ) res.json(rows) } catch (error) { console.error('Worker history error:', error) res.status(500).json({ message: 'Database error fetching history.' }) } }) app.put('/api/worker/change-password', authenticateJWT, async (req, res) => { try { const { userId } = req.user // Get user ID from JWT const { currentPassword, newPassword } = req.body if (!currentPassword || !newPassword) { return res.status(400).json({ message: 'Current password and new password are required.' }) } if (newPassword.length < 6) { return res.status(400).json({ message: 'New password must be at least 6 characters long.' }) } // Get user's current password hash const [rows] = await db.execute('SELECT password_hash FROM workers WHERE id = ?', [userId]) if (rows.length === 0) { return res.status(404).json({ message: 'User not found.' }) } const user = rows[0] // Verify current password const passwordMatch = await bcrypt.compare(currentPassword, user.password_hash) if (!passwordMatch) { return res.status(401).json({ message: 'Incorrect current password.' }) } // Hash new password const saltRounds = 10 const newHashedPassword = await bcrypt.hash(newPassword, saltRounds) // Update password in DB await db.execute('UPDATE workers SET password_hash = ? WHERE id = ?', [ newHashedPassword, userId, ]) res.json({ message: 'Password updated successfully.' }) } catch (error) { console.error('Change password error:', error) res.status(500).json({ message: 'Database error during password change.' }) } }) // Manager: PUT (Update) a Worker's Password app.put('/api/managers/workers/:workerId/password', authenticateJWT, async (req, res) => { try { // Ensure the user performing the action is a manager if (req.user.role !== 'manager') { return res .status(403) .json({ message: 'Forbidden: You do not have permission to perform this action.' }) } const { workerId } = req.params const { newPassword } = req.body if (!newPassword || newPassword.length < 6) { return res.status(400).json({ message: 'Password must be at least 6 characters long.' }) } const saltRounds = 10 const hashedPassword = await bcrypt.hash(newPassword, saltRounds) const [result] = await db.execute( "UPDATE workers SET password_hash = ? WHERE id = ? AND role = 'worker'", [hashedPassword, workerId], ) if (result.affectedRows === 0) { return res .status(404) .json({ message: 'Worker not found or you cannot change the password for this user.' }) } res.status(200).json({ message: 'Password updated successfully.' }) } catch (error) { console.error('Update password error:', error) res.status(500).json({ message: 'Database error while updating password.' }) } }) // GET all tags app.get('/api/managers/tags', authenticateJWT, async (req, res) => { try { const [tags] = await db.execute('SELECT * FROM tags ORDER BY tag_name ASC') res.json(tags) } catch (error) { console.error('Get tags error:', error) res.status(500).json({ message: 'Database error fetching tags.' }) } }) // POST a new tag app.post('/api/managers/tags', authenticateJWT, async (req, res) => { try { const { tag_name } = req.body if (!tag_name) { return res.status(400).json({ message: 'Tag name is required.' }) } const [result] = await db.execute('INSERT INTO tags (tag_name) VALUES (?)', [tag_name]) res.status(201).json({ id: result.insertId, tag_name }) } catch (error) { if (error.code === 'ER_DUP_ENTRY') { return res.status(409).json({ message: 'This tag already exists.' }) } console.error('Add tag error:', error) res.status(500).json({ message: 'Database error adding tag.' }) } }) // NEW: DELETE a tag app.delete('/api/managers/tags/:id', authenticateJWT, async (req, res) => { try { const { id } = req.params // Optional: Check if the user is a manager before allowing deletion if (req.user.role !== 'manager') { return res.status(403).json({ message: 'Forbidden: Only managers can delete tags.' }) } // Delete the tag from the 'tags' table. // If 'worker_tags' table has ON DELETE CASCADE for tag_id, // related entries in 'worker_tags' will automatically be removed. const [result] = await db.execute('DELETE FROM tags WHERE id = ?', [id]) if (result.affectedRows === 0) { return res.status(404).json({ message: 'Tag not found.' }) } res.status(204).send() // 204 No Content for successful deletion } catch (error) { console.error('Delete tag error:', error) res.status(500).json({ message: 'Database error deleting tag.' }) } }) // POST to assign a tag to a worker app.post('/api/managers/workers/:workerId/tags', authenticateJWT, async (req, res) => { try { const { workerId } = req.params const { tagId } = req.body // Expects a single tag ID if (!tagId) { return res.status(400).json({ message: 'Tag ID is required.' }) } // INSERT IGNORE prevents errors if the tag is already assigned to the worker await db.query('INSERT IGNORE INTO worker_tags (worker_id, tag_id) VALUES (?, ?)', [ workerId, tagId, ]) res.status(200).json({ message: 'Tag assigned successfully.' }) } catch (error) { console.error('Assign tag error:', error) res.status(500).json({ message: 'Database error assigning tag.' }) } }) // DELETE to remove a tag from a worker app.delete('/api/managers/workers/:workerId/tags/:tagId', authenticateJWT, async (req, res) => { try { const { workerId, tagId } = req.params await db.query('DELETE FROM worker_tags WHERE worker_id = ? AND tag_id = ?', [ workerId, tagId, ]) res.status(204).send() // 204 No Content for successful deletion } catch (error) { console.error('Remove tag error:', error) res.status(500).json({ message: 'Database error removing tag.' }) } }) // Find this endpoint in your server.js and replace it with the code below. // Manager: GET All Workers (FIXED for older MySQL versions) app.get('/api/managers/workers', authenticateJWT, async (req, res) => { try { const { search = '', page = 1, limit = 20, tags = '' } = req.query const offset = (parseInt(page) - 1) * parseInt(limit) const searchTerm = `%${search}%` const tagIds = tags .split(',') .filter((id) => id) .map(Number) const hasTagFilter = tagIds.length > 0 // Base queries let baseQuery = ` SELECT w.id, w.username, w.full_name, w.created_at, (SELECT GROUP_CONCAT(t.tag_name SEPARATOR ', ') FROM worker_tags wt_sub JOIN tags t ON wt_sub.tag_id = t.id WHERE wt_sub.worker_id = w.id) as tags FROM workers w ` let countQuery = `SELECT COUNT(DISTINCT w.id) as totalCount FROM workers w` // Parameters for the queries const params = [] const countParams = [] // Join with worker_tags if filtering if (hasTagFilter) { const joinClause = ` JOIN worker_tags wt ON w.id = wt.worker_id` baseQuery += joinClause countQuery += joinClause } // Common WHERE clause const whereClause = ` WHERE w.role = 'worker' AND (w.full_name LIKE ? OR w.username LIKE ?)` baseQuery += whereClause countQuery += whereClause params.push(searchTerm, searchTerm) countParams.push(searchTerm, searchTerm) // Add tag filtering logic if (hasTagFilter) { const tagPlaceholders = tagIds.map(() => '?').join(',') const tagFilterClause = ` AND wt.tag_id IN (${tagPlaceholders})` baseQuery += tagFilterClause countQuery += tagFilterClause // Add the tag IDs to the parameters individually params.push(...tagIds) countParams.push(...tagIds) // --- FIX END --- } // Grouping and pagination for the main query if (hasTagFilter) { baseQuery += ` GROUP BY w.id HAVING COUNT(DISTINCT wt.tag_id) = ?` params.push(tagIds.length) } baseQuery += ` ORDER BY w.created_at DESC LIMIT ? OFFSET ?` params.push(parseInt(limit), offset) // Execute queries const [workers] = await db.execute(baseQuery, params) const [[{ totalCount }]] = await db.execute(countQuery, countParams) res.json({ workers, totalCount }) } catch (error) { // This is the error you are seeing console.error('Get workers error:', error) res.status(500).json({ message: 'Database error fetching workers.' }) } }) // Manager: POST (Add new) Worker app.post('/api/managers/workers', authenticateJWT, async (req, res) => { try { const { username, password, fullName, role = 'worker' } = req.body if (!username || !password || !fullName) { return res.status(400).json({ message: 'Username, password, and full name are required.' }) } if (!['worker', 'manager'].includes(role)) { return res.status(400).json({ message: 'Invalid role specified.' }) } const saltRounds = 10 const hashedPassword = await bcrypt.hash(password, saltRounds) const [result] = await db.execute( 'INSERT INTO workers (username, password_hash, full_name, role) VALUES (?, ?, ?, ?)', [username, hashedPassword, fullName, role], // Pass role to query ) res.status(201).json({ id: result.insertId, username, fullName, role }) } catch (error) { if (error.code === 'ER_DUP_ENTRY') { return res.status(409).json({ message: 'Username already exists.' }) } console.error('Add worker error:', error) res.status(500).json({ message: 'Database error adding worker.' }) } }) // Manager: DELETE Worker app.delete('/api/managers/workers/:id', authenticateJWT, async (req, res) => { try { const { id } = req.params const [result] = await db.execute("DELETE FROM workers WHERE id = ? AND role = 'worker'", [ id, ]) if (result.affectedRows === 0) { return res.status(404).json({ message: 'Worker not found or user is not a worker.' }) } res.status(204).send() } catch (error) { console.error('Delete worker error:', error) res.status(500).json({ message: 'Database error deleting worker.' }) } }) // --- NEW --- Manager: POST (Add Manual Attendance Record) // Note: For this to work, you may need to alter your database table: // ALTER TABLE clock_records ADD COLUMN notes TEXT; app.post('/api/managers/add-record', authenticateJWT, async (req, res) => { try { const { workerId, eventType, timestamp, notes } = req.body if (!workerId || !eventType || !timestamp) { return res .status(400) .json({ message: 'Worker ID, event type, and timestamp are required.' }) } // Check last event to prevent adding a duplicate event type const [lastEventRows] = await db.execute( 'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [workerId], ) if (lastEventRows.length > 0 && lastEventRows[0].event_type === eventType) { const status = eventType === 'clock_in' ? 'in' : 'out' return res.status(409).json({ message: `Worker is already clocked ${status}.` }) } // --- THIS IS THE FIX --- const sanitizedTimestamp = timestamp.replace('T', ' ') await db.execute( 'INSERT INTO clock_records (worker_id, event_type, timestamp, notes, qr_code_id, latitude, longitude) VALUES (?, ?, ?, ?, NULL, NULL, NULL)', [workerId, eventType, sanitizedTimestamp, notes], ) res.status(201).json({ message: 'Manual record added successfully.' }) } catch (error) { console.error('Add manual record error:', error) res.status(500).json({ message: 'Database error adding manual record.' }) } }) // Manager: GET Attendance Records app.get('/api/managers/attendance-records', authenticateJWT, async (req, res) => { try { const { workerIds, startDate, endDate, format } = req.query if (!workerIds) { return res.status(400).json({ message: 'Worker IDs are required.' }) } const idsArray = workerIds.split(',').map(Number) if (idsArray.length === 0) return res.json([]) const placeholders = idsArray.map(() => '?').join(',') // MODIFIED: Use LEFT JOIN and COALESCE to handle manual entries, and select `notes` let query = `SELECT cr.id, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName, cr.latitude, cr.longitude, cr.notes FROM clock_records cr LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id JOIN workers w ON cr.worker_id = w.id WHERE cr.worker_id IN (${placeholders})`; const params = [...idsArray] if (startDate && endDate) { const endOfDay = new Date(endDate) endOfDay.setHours(23, 59, 59, 999) query += ' AND cr.timestamp BETWEEN ? AND ?' params.push(startDate, endOfDay) } query += ' ORDER BY w.full_name, cr.timestamp DESC' const [rows] = await db.execute(query, params) if (format === 'csv') { // MODIFIED: Add 'notes' to CSV export const json2csvParser = new Parser({ fields: ['full_name', 'event_type', 'timestamp', 'qrCodeUsedName', 'notes'], }) const csv = json2csvParser.parse(rows) res.header('Content-Type', 'text/csv') res.attachment(`attendance-report-${new Date().toISOString().split('T')[0]}.csv`) return res.send(csv) } res.json(rows) } catch (error) { console.error('Attendance records error:', error) res.status(500).json({ message: 'Database error fetching attendance records.' }) } }) // Manager: GET QR Codes app.get('/api/managers/qr-codes', authenticateJWT, async (req, res) => { try { const [rows] = await db.execute( 'SELECT id, name, is_active, created_at FROM qr_codes ORDER BY created_at DESC', ) res.json(rows) } catch (error) { console.error('Get QR codes error:', error) res.status(500).json({ message: 'Database error fetching QR codes.' }) } }) // Manager: POST QR Code app.post('/api/managers/qr-codes', authenticateJWT, async (req, res) => { try { const { name } = req.body if (!name) return res.status(400).json({ message: 'QR Code name is required.' }) const newQrCode = { id: uuidv4(), name, isActive: true } await db.execute('INSERT INTO qr_codes (id, name, is_active) VALUES (?, ?, ?)', [ newQrCode.id, newQrCode.name, newQrCode.isActive, ]) res .status(201) .json({ id: newQrCode.id, name: newQrCode.name, is_active: newQrCode.isActive }) } catch (error) { console.error('Add QR code error:', error) res.status(500).json({ message: 'Database error adding QR code.' }) } }) // Manager: PUT QR Code app.put('/api/managers/qr-codes/:id', authenticateJWT, async (req, res) => { try { const { id } = req.params const { isActive } = req.body if (typeof isActive !== 'boolean') return res.status(400).json({ message: 'isActive must be a boolean.' }) const [result] = await db.execute('UPDATE qr_codes SET is_active = ? WHERE id = ?', [ isActive, id, ]) if (result.affectedRows === 0) return res.status(404).json({ message: 'QR Code not found.' }) res.json({ id, isActive }) } catch (error) { console.error('Update QR code error:', error) res.status(500).json({ message: 'Database error updating QR code.' }) } }) // Manager: DELETE QR Code app.delete('/api/managers/qr-codes/:id', authenticateJWT, async (req, res) => { try { const { id } = req.params const [result] = await db.execute('DELETE FROM qr_codes WHERE id = ?', [id]) if (result.affectedRows === 0) return res.status(404).json({ message: 'QR Code not found.' }) res.status(204).send() } catch (error) { console.error('Delete QR code error:', error) res.status(500).json({ message: 'Database error deleting QR code.' }) } }) // Manager: GET single worker's details app.get('/api/managers/worker/:id', authenticateJWT, async (req, res) => { try { const { id } = req.params const [rows] = await db.execute( "SELECT full_name FROM workers WHERE id = ? AND role = 'worker'", [id], ) if (rows.length > 0) { res.json(rows[0]) } else { res.status(404).json({ message: 'Worker not found.' }) } } catch (error) { console.error('Get single worker error:', error) res.status(500).json({ message: 'Database error fetching worker details.' }) } }) // --- NEW NATIVE FEATURES API ENDPOINTS --- // Location Update Endpoint - OPTIMIZED: Removed continuous geofence checking app.post('/api/location/update', authenticateJWT, async (req, res) => { try { const { userId, latitude, longitude, checkGeofence } = req.body if (!userId || !latitude || !longitude) { return res.status(400).json({ message: 'User ID, latitude, and longitude are required.' }) } // OPTIMIZATION: Only check geofence when explicitly requested // This reduces unnecessary processing on every location update if (checkGeofence === true) { try { const userLocation = point([longitude, latitude]); const isWithinGeofence = booleanPointInPolygon(userLocation, geofence); if (!isWithinGeofence) { // User is outside the geofence - log security alert silently const distance = pointToLineDistance(userLocation, geofence.geometry.coordinates[0], { units: 'meters' }); const alertData = { latitude: latitude, longitude: longitude, timestamp: new Date().toISOString(), distance_from_geofence: distance.toFixed(2), check_type: 'scheduled_update' // Indicate this was a scheduled check }; // Log geofence violation to security_alerts table await logSecurityAlert(userId, 'geofence_violation', alertData, db); console.log(`OPTIMIZED: Geofence violation detected for user ${userId}: ${distance.toFixed(2)} meters outside boundary`); } else { console.log(`OPTIMIZED: User ${userId} within geofence during scheduled check`); } } catch (geofenceError) { console.error('OPTIMIZED: Error checking geofence:', geofenceError); // Continue with location update even if geofence check fails } } // OPTIMIZED: Simplified location update - only essential fields // No need for timestamp conversion as we use created_at with NOW() await db.execute( 'INSERT INTO location_updates (user_id, longitude, latitude, created_at) VALUES (?, ?, ?, NOW())', [userId, longitude, latitude] ) res.json({ message: 'Location updated successfully' }) } catch (error) { console.error('Location update error:', error) res.status(500).json({ message: 'Database error during location update.' }) } }) // Device Registration Endpoint app.post('/api/device/register', authenticateJWT, async (req, res) => { try { const { userId, deviceUuid, deviceInfo } = req.body if (!userId || !deviceUuid) { return res.status(400).json({ message: 'User ID and device UUID are required.' }) } const result = await registerDeviceForUser(userId, deviceUuid, deviceInfo, db) if (result.success) { res.json({ message: result.message, success: true }) } else { res.status(409).json({ message: result.message, success: false }) } } catch (error) { console.error('Device registration error:', error) res.status(500).json({ message: 'Database error during device registration.', success: false }) } }) // Device Validation Endpoint app.post('/api/device/validate', authenticateJWT, async (req, res) => { try { const { userId, deviceUuid } = req.body if (!userId || !deviceUuid) { return res.status(400).json({ message: 'User ID and device UUID are required.' }) } const validation = await validateDeviceForUser(userId, deviceUuid, db) res.json({ valid: validation.valid, message: validation.message }) } catch (error) { console.error('Device validation error:', error) res.status(500).json({ message: 'Database error during device validation.', valid: false }) } }) // Security Check Endpoint - COMMENTED OUT FOR SERVER-SIDE SECURITY PREFERENCE // Client-side security computation removed per user preference for server-side security /* app.post('/api/security/check', authenticateJWT, async (req, res) => { try { const { userId, timestamp, deviceInfo, securityCheck } = req.body if (!userId || !securityCheck) { return res.status(400).json({ message: 'User ID and security check data are required.' }) } // Calculate risk score let riskScore = 0 if (securityCheck.suspiciousApps && securityCheck.suspiciousApps.totalSuspiciousApps > 0) { riskScore += securityCheck.suspiciousApps.totalSuspiciousApps * 10 } if (securityCheck.riskLevel === 'high') { riskScore += 50 } else if (securityCheck.riskLevel === 'medium') { riskScore += 25 } // Convert timestamp to MySQL-compatible format let mysqlTimestamp if (timestamp) { if (typeof timestamp === 'string' && timestamp.includes('T')) { // Handle ISO 8601 format (e.g., '2025-07-04T09:00:49.192Z') // Convert to MySQL DATETIME format by replacing 'T' with ' ' and removing 'Z' mysqlTimestamp = timestamp.replace('T', ' ').replace('Z', '') } else if (timestamp instanceof Date) { // Handle Date object mysqlTimestamp = timestamp.toISOString().replace('T', ' ').replace('Z', '') } else { // Fallback to current time for invalid formats mysqlTimestamp = new Date().toISOString().replace('T', ' ').replace('Z', '') } } else { // Use current time if no timestamp provided mysqlTimestamp = new Date().toISOString().replace('T', ' ').replace('Z', '') } // Store security check results await db.execute( 'INSERT INTO security_checks (user_id, timestamp, device_info, security_data, risk_level, risk_score, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())', [userId, mysqlTimestamp, JSON.stringify(deviceInfo), JSON.stringify(securityCheck), securityCheck.riskLevel, riskScore] ) // Check if risk level is too high if (riskScore >= 50) { // Log high-risk event await db.execute( 'INSERT INTO security_alerts (user_id, alert_type, alert_data, severity, created_at) VALUES (?, ?, ?, ?, NOW())', [userId, 'high_risk_device', JSON.stringify({ riskScore, securityCheck }), 'high'] ) return res.status(403).json({ message: 'Device security risk too high. Please contact administrator.', riskLevel: securityCheck.riskLevel, riskScore: riskScore }) } res.json({ message: 'Security check completed successfully', riskLevel: securityCheck.riskLevel, riskScore: riskScore, status: 'approved' }) } catch (error) { console.error('Security check error:', error) res.status(500).json({ message: 'Database error during security check.' }) } }) */ // Get Security Status Endpoint app.get('/api/security/status/:userId', authenticateJWT, async (req, res) => { try { const { userId } = req.params // Get latest security check const [securityRows] = await db.execute( 'SELECT * FROM security_checks WHERE user_id = ? ORDER BY created_at DESC LIMIT 1', [userId] ) // Get recent security alerts const [alertRows] = await db.execute( 'SELECT * FROM security_alerts WHERE user_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL 7 DAY) ORDER BY created_at DESC', [userId] ) res.json({ latestSecurityCheck: securityRows[0] || null, recentAlerts: alertRows, securityStatus: securityRows.length > 0 ? securityRows[0].risk_level : 'unknown' }) } catch (error) { console.error('Get security status error:', error) res.status(500).json({ message: 'Database error fetching security status.' }) } }) // Get App Blacklist Endpoint app.get('/api/security/app-blacklist', authenticateJWT, async (req, res) => { try { const [rows] = await db.execute('SELECT package_name FROM app_blacklist'); const packageNames = rows.map(row => row.package_name); res.json(packageNames); } catch (error) { console.error('Get app blacklist error:', error); res.status(500).json({ message: 'Database error fetching app blacklist.' }); } }); // --- Server Start --- const httpPort = process.env.HTTP_PORT || 3000 const httpsPort = process.env.HTTPS_PORT || 3443 const sslEnabled = process.env.SSL_ENABLED === 'true' if (sslEnabled) { try { // Check if SSL certificate files exist const currentDir = path.dirname(fileURLToPath(import.meta.url)) const keyPath = path.join(currentDir, 'key.pem') const certPath = path.join(currentDir, 'cert.pem') console.log('Resolved keyPath:', keyPath) console.log('Resolved certPath:', certPath) if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) { console.error('SSL certificate or key file not found. Falling back to HTTP.') startHttpServer() return } const httpsOptions = { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath), } // Start HTTPS server const httpsServer = https.createServer(httpsOptions, app) httpsServer.listen(httpsPort, '0.0.0.0', () => { console.log(`🔒 HTTPS Server is running on https://localhost:${httpsPort}`) if (process.env.SERVER_IP) { console.log(`🔒 HTTPS Server is also available on https://${process.env.SERVER_IP}:${httpsPort}`) } console.log('📱 For Android testing, use the IP address URL') }) // Optional: Start HTTP server that redirects to HTTPS const httpApp = express() httpApp.use((req, res) => { const host = req.headers.host?.split(':')[0] || 'localhost' const redirectUrl = `https://${host}:${httpsPort}${req.url}` res.redirect(301, redirectUrl) }) const httpServer = http.createServer(httpApp) httpServer.listen(httpPort, '0.0.0.0', () => { console.log(`🔄 HTTP Server (redirect) is running on http://localhost:${httpPort}`) }) // Graceful shutdown process.on('SIGTERM', () => { console.log('Shutting down servers...') httpsServer.close(() => { console.log('HTTPS server closed') }) httpServer.close(() => { console.log('HTTP server closed') }) }) } catch (error) { console.error('❌ Failed to start HTTPS server:', error.message) console.log('Falling back to HTTP server...') startHttpServer() } } else { startHttpServer() } function startHttpServer() { const httpServer = http.createServer(app) httpServer.listen(httpPort, '0.0.0.0', () => { console.log(`🌐 HTTP Server is running on http://localhost:${httpPort}`) if (process.env.SERVER_IP) { console.log(`🌐 HTTP Server is also available on http://${process.env.SERVER_IP}:${httpPort}`) } console.log('⚠️ Using HTTP - HTTPS recommended for mobile testing') }) } } startServer()