import express from 'express'; import { point, polygon, booleanPointInPolygon, pointToLineDistance } from '@turf/turf'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; // Removed unused import 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(db) { const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD format const [rows] = await db.execute('SELECT 1 FROM enabled_dates WHERE enabled_date = ? LIMIT 1', [today]); return rows.length > 0; } export default function(db) { 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 { 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' }); } // console.log(`Device UUID registered for worker ${user.id}: ${deviceUuid}`); } 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 }); }); 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 }; // Correctly map userId to id next(); }); } else { res.status(401).json({ message: 'Authorization header required' }); } }; router.use(authenticateJWT); // Definitive version with distance calculation and specific error messages // Definitive version with distance calculation and specific error messages router.post('/clock', async (req, res) => { try { const { userId, eventType, qrCodeValue, latitude, longitude } = req.body; const currentTimestamp = new Date().toISOString().slice(0, 19).replace('T', ' '); // 1. Kill Switch Enforcement 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", ?, ?, ?, ?, ?)', [userId, qrCodeValue, latitude, longitude, note, currentTimestamp] ); return res.status(403).json({ message: 'error.clockingDisabled' }); } // 2. Geofence Validation with Distance Calculation 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", ?, ?, ?, ?, ?)', [userId, qrCodeValue, latitude, longitude, note, currentTimestamp]); 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); // Save for distance calculation 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", ?, ?, ?, ?, ?)', [userId, qrCodeValue, latitude, longitude, note, currentTimestamp]); 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 (?, ?, ?, ?, ?, ?)', [userId, eventType, qrCodeValue, latitude, longitude, currentTimestamp] ); 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' }); } }); router.get('/workers/:id', async (req, res) => { 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]); }); router.get('/worker/status/:userId', async (req, res) => { 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' }); }); router.get('/worker/clock-history/:userId', async (req, res) => { 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); }); router.put('/worker/change-password', async (req, res) => { 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.' }); }); 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 { userId, deviceUuid } = req.body; const result = await validateDeviceForUser(userId, deviceUuid, db); res.status(result.valid ? 200 : 409).json(result); }); router.post('/device/validate', async (req, res) => { const { userId, deviceUuid } = req.body; const result = await validateDeviceForUser(userId, deviceUuid, db); res.json(result); }); router.get('/security/status/:userId', async (req, res) => { 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, }); }); router.get('/security/app-blacklist', async (req, res) => { const [rows] = await db.execute('SELECT package_name FROM app_blacklist'); res.json(rows.map(row => row.package_name)); }); return router; }