import express from 'express'; import { point, polygon, booleanPointInPolygon, pointToLineDistance } from '@turf/turf'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { getConnection } from './pool.js'; async function validateDeviceForUser(userId, deviceUuid, db) { const [userRows] = await db.execute('SELECT device_uuid FROM workers WHERE id = ?', [userId]); if (userRows.length === 0) return { valid: false, message: 'User not found' }; const { device_uuid } = userRows[0]; if (!device_uuid) { await db.execute('UPDATE workers SET device_uuid = ? WHERE id = ?', [deviceUuid, userId]); return { valid: true, message: 'Device registered successfully' }; } return { valid: device_uuid === deviceUuid, message: 'Device validation failed' }; } async function isClockingEnabled(conn) { const [rows] = await conn.execute( 'SELECT 1 FROM enabled_dates WHERE enabled_date = CURDATE() LIMIT 1' ); return rows.length > 0; } export default function() { const router = express.Router(); // Set DEVICE_UUID_ENABLED to false to completely disable device UUID checking const DEVICE_UUID_ENABLED = false; const REQUIRE_DEVICE_FOR_WORKERS = true; const AUTO_REGISTER_NEW_DEVICES = true; router.post('/auth/login', async (req, res) => { const db = await getConnection(); try { const { username, password, deviceUuid } = req.body; const [rows] = await db.execute('SELECT id, role, password_hash, status FROM workers WHERE username = ?', [username]); if (rows.length === 0) { return res.status(401).json({ message: 'Invalid credentials' }); } const user = rows[0]; // Check if the user's status is 'active' if (user.status !== 'active') { return res.status(401).json({ message: 'Invalid credentials' }); } const passwordMatch = await bcrypt.compare(password, user.password_hash); if (!passwordMatch) { return res.status(401).json({ message: 'Invalid credentials' }); } // Device UUID handling - controlled by configuration flags above if (DEVICE_UUID_ENABLED && user.role === 'worker') { const [deviceRows] = await db.execute('SELECT device_uuid FROM workers WHERE id = ?', [user.id]); const existingDeviceUuid = deviceRows[0].device_uuid; if (existingDeviceUuid) { if (deviceUuid && deviceUuid !== existingDeviceUuid) { return res.status(403).json({ message: 'deviceMismatch' }); } else if (!deviceUuid) { return res.status(403).json({ message: 'useMobileApp' }); } } else { // User has no registered device if (deviceUuid && AUTO_REGISTER_NEW_DEVICES) { const deviceResult = await validateDeviceForUser(user.id, deviceUuid, db); if (!deviceResult.valid) { return res.status(500).json({ message: 'deviceRegistrationFailed' }); } } else if (!deviceUuid && REQUIRE_DEVICE_FOR_WORKERS) { return res.status(403).json({ message: 'deviceRequired' }); } } } // Managers can always login, workers without device_uuid can login const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' }); res.json({ token }); } catch (error) { console.error('Login error:', error); res.status(500).json({ message: 'Server error during login' }); } finally { db.release(); } }); 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: 'Invalid or expired token' }); } req.user = { ...user, id: user.userId }; next(); }); } else { res.status(401).json({ message: 'Authorization header required' }); } }; router.use(authenticateJWT); router.post('/clock', async (req, res) => { const db = await getConnection(); try { const { userId, eventType, qrCodeValue, latitude, longitude } = req.body; // 1) Kill Switch — now evaluated in the session's local day const clockingAllowed = await isClockingEnabled(db); if (!clockingAllowed) { const note = 'Clock-in/out function is not enabled for today.'; await db.execute( `INSERT INTO clock_records (worker_id, event_type, qr_code_id, latitude, longitude, notes, timestamp) VALUES (?, "failed", ?, ?, ?, ?, CURRENT_TIME())`, [userId, qrCodeValue, latitude, longitude, note] ); return res.status(403).json({ message: 'error.clockingDisabled' }); } // 2) Geofence Validation if (latitude != null && longitude != null) { const [activeFences] = await db.execute('SELECT coordinates FROM geofences WHERE is_active = 1'); if (activeFences.length === 0) { const note = 'Cannot clock in: No active work area is defined.'; await db.execute( `INSERT INTO clock_records (worker_id, event_type, qr_code_id, latitude, longitude, notes, timestamp) VALUES (?, "failed", ?, ?, ?, ?, CURRENT_TIME())`, [userId, qrCodeValue, latitude, longitude, note] ); return res.status(403).json({ message: 'error.noActiveGeofence' }); } const userLocation = point([longitude, latitude]); const parsedPolygons = []; let isInside = false; for (const fence of activeFences) { try { if (!fence.coordinates) continue; const coordinates = JSON.parse(fence.coordinates); const fencePolygon = polygon([coordinates]); parsedPolygons.push(fencePolygon); if (booleanPointInPolygon(userLocation, fencePolygon)) { isInside = true; break; } } catch (e) { console.error('Could not parse geofence coordinates:', { coordinates: fence.coordinates, error: e }); } } if (!isInside) { let minDistance = Infinity; for (const p of parsedPolygons) { const distance = pointToLineDistance(userLocation, p.geometry.coordinates[0], { units: 'meters' }); if (distance < minDistance) minDistance = distance; } const distanceString = minDistance.toFixed(2); const note = `Outside geofence by ${distanceString}m`; await db.execute( `INSERT INTO clock_records (worker_id, event_type, qr_code_id, latitude, longitude, notes, timestamp) VALUES (?, "failed", ?, ?, ?, ?, CURRENT_TIME())`, [userId, qrCodeValue, latitude, longitude, note] ); return res.status(403).json({ message: `error.outsideGeofence|${distanceString}` }); } } // 3) QR Code and Status Validation if (qrCodeValue !== 'FORCE_CLOCK_OUT') { const [qrRows] = await db.execute('SELECT is_active FROM qr_codes WHERE id = ?', [qrCodeValue]); if (qrRows.length === 0 || !qrRows[0].is_active) { return res.status(400).json({ message: 'error.invalidQrCode' }); } } const [lastEvent] = await db.execute( 'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1', [userId] ); if (lastEvent.length > 0 && lastEvent[0].event_type === eventType) { const errorKey = eventType === 'clock_in' ? 'error.alreadyClockedIn' : 'error.alreadyClockedOut'; return res.status(400).json({ message: errorKey }); } // 4) Record Successful Event await db.execute( `INSERT INTO clock_records (worker_id, event_type, qr_code_id, latitude, longitude, timestamp) VALUES (?, ?, ?, ?, ?, CURRENT_TIME())`, [userId, eventType, qrCodeValue, latitude, longitude] ); res.status(201).json({ message: 'Clock event recorded.' }); } catch (error) { console.error('!!! CRITICAL ERROR in /clock route !!!:', error); res.status(500).json({ message: 'error.criticalServer' }); } finally { db.release(); } }); router.get('/workers/:id', async (req, res) => { const db = await getConnection(); 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) { return res.status(404).json({ message: 'Worker not found.' }); } res.json(rows[0]); } catch (error) { console.error('Get worker error:', error); res.status(500).json({ message: 'Server error fetching worker' }); } finally { db.release(); } }); router.get('/worker/status/:userId', async (req, res) => { const db = await getConnection(); 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]); res.json({ eventType: rows.length > 0 ? rows[0].event_type : 'clock_out' }); } catch (error) { console.error('Get worker status error:', error); res.status(500).json({ message: 'Server error fetching worker status' }); } finally { db.release(); } }); router.get('/worker/clock-history/:userId', async (req, res) => { const db = await getConnection(); try { const { userId } = req.params; 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('Get clock history error:', error); res.status(500).json({ message: 'Server error fetching clock history' }); } finally { db.release(); } }); router.put('/worker/change-password', async (req, res) => { const db = await getConnection(); try { const { userId } = req.user; const { currentPassword, newPassword } = req.body; if (!currentPassword || !newPassword || newPassword.length < 6) { return res.status(400).json({ message: 'Invalid input.' }); } const [rows] = await db.execute('SELECT password_hash FROM workers WHERE id = ?', [userId]); const passwordMatch = await bcrypt.compare(currentPassword, rows[0].password_hash); if (!passwordMatch) { return res.status(401).json({ message: 'Incorrect current password.' }); } const newHashedPassword = await bcrypt.hash(newPassword, 10); 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: 'Server error changing password' }); } finally { db.release(); } }); router.post('/location/update', async (req, res) => { // Do nothing, always return location updated res.json({ message: 'Location updated.' }); }); router.post('/device/register', async (req, res) => { const db = await getConnection(); try { const { userId, deviceUuid } = req.body; const result = await validateDeviceForUser(userId, deviceUuid, db); res.status(result.valid ? 200 : 409).json(result); } catch (error) { console.error('Device register error:', error); res.status(500).json({ message: 'Server error registering device' }); } finally { db.release(); } }); router.post('/device/validate', async (req, res) => { const db = await getConnection(); try { const { userId, deviceUuid } = req.body; const result = await validateDeviceForUser(userId, deviceUuid, db); res.json(result); } catch (error) { console.error('Device validate error:', error); res.status(500).json({ message: 'Server error validating device' }); } finally { db.release(); } }); router.get('/security/status/:userId', async (req, res) => { const db = await getConnection(); try { const { userId } = req.params; const [securityRows] = await db.execute('SELECT * FROM security_checks WHERE user_id = ? ORDER BY created_at DESC LIMIT 1', [userId]); const [alertRows] = await db.execute('SELECT * FROM security_alerts WHERE user_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL 7 DAY)', [userId]); res.json({ latestSecurityCheck: securityRows[0] || null, recentAlerts: alertRows, }); } catch (error) { console.error('Security status error:', error); res.status(500).json({ message: 'Server error fetching security status' }); } finally { db.release(); } }); router.get('/security/app-blacklist', async (req, res) => { const db = await getConnection(); try { const [rows] = await db.execute('SELECT package_name FROM app_blacklist'); res.json(rows.map(row => row.package_name)); } catch (error) { console.error('App blacklist error:', error); res.status(500).json({ message: 'Server error fetching app blacklist' }); } finally { db.release(); } }); return router; }