backend done
This commit is contained in:
+115
-86
@@ -2,7 +2,12 @@ import express from 'express';
|
||||
import { point, polygon, booleanPointInPolygon, pointToLineDistance } from '@turf/turf';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
// Removed unused import
|
||||
import { db } from './server.js';
|
||||
import { withTzSession } from './middleware/withTzSession.js';
|
||||
|
||||
|
||||
// Map IANA (no DST for KL/Jakarta)
|
||||
const sessionOffset = (iana) => (iana === 'Asia/Jakarta' ? '+07:00' : '+08:00');
|
||||
|
||||
async function validateDeviceForUser(userId, deviceUuid, db) {
|
||||
const [userRows] = await db.execute('SELECT device_uuid FROM workers WHERE id = ?', [userId]);
|
||||
@@ -15,9 +20,10 @@ async function validateDeviceForUser(userId, deviceUuid, db) {
|
||||
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]);
|
||||
async function isClockingEnabled(conn) {
|
||||
const [rows] = await conn.execute(
|
||||
'SELECT 1 FROM enabled_dates WHERE enabled_date = CURDATE() LIMIT 1'
|
||||
);
|
||||
return rows.length > 0;
|
||||
}
|
||||
|
||||
@@ -94,95 +100,118 @@ export default function(db) {
|
||||
};
|
||||
|
||||
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', ' ');
|
||||
import { db } from './server.js'; // ensure this exists up top
|
||||
|
||||
// 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' });
|
||||
}
|
||||
router.post('/clock', async (req, res) => {
|
||||
// NEW: borrow a connection so we can set session time_zone
|
||||
const conn = await db.getConnection();
|
||||
try {
|
||||
const { userId, eventType, qrCodeValue, latitude, longitude } = req.body;
|
||||
|
||||
// 2. Geofence Validation with Distance Calculation
|
||||
if (latitude != null && longitude != null) {
|
||||
const [activeFences] = await db.execute('SELECT coordinates FROM geofences WHERE is_active = 1');
|
||||
// NEW: set session time_zone from header (defaults to KL)
|
||||
const iana = req.headers['x-user-timezone'] || 'Asia/Kuala_Lumpur';
|
||||
await conn.query('SET time_zone = ?', [sessionOffset(iana)]);
|
||||
|
||||
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]
|
||||
// 1) Kill Switch — now evaluated in the session's local day
|
||||
const clockingAllowed = await isClockingEnabled(conn); // CHANGED: pass conn
|
||||
if (!clockingAllowed) {
|
||||
const note = 'Clock-in/out function is not enabled for today.';
|
||||
await conn.execute( // CHANGED: use conn
|
||||
`INSERT INTO clock_records
|
||||
(worker_id, event_type, qr_code_id, latitude, longitude, notes, timestamp)
|
||||
VALUES (?, "failed", ?, ?, ?, ?, CONVERT_TZ(NOW(), @@session.time_zone, '+00:00'))`,
|
||||
[userId, qrCodeValue, latitude, longitude, note]
|
||||
);
|
||||
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' });
|
||||
return res.status(403).json({ message: 'error.clockingDisabled' });
|
||||
}
|
||||
});
|
||||
|
||||
// 2) Geofence Validation (unchanged logic, just switch db -> conn)
|
||||
if (latitude != null && longitude != null) {
|
||||
const [activeFences] = await conn.execute('SELECT coordinates FROM geofences WHERE is_active = 1'); // CHANGED
|
||||
|
||||
if (activeFences.length === 0) {
|
||||
const note = 'Cannot clock in: No active work area is defined.';
|
||||
await conn.execute( // CHANGED
|
||||
`INSERT INTO clock_records
|
||||
(worker_id, event_type, qr_code_id, latitude, longitude, notes, timestamp)
|
||||
VALUES (?, "failed", ?, ?, ?, ?, CONVERT_TZ(NOW(), @@session.time_zone, '+00:00'))`,
|
||||
[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 conn.execute( // CHANGED
|
||||
`INSERT INTO clock_records
|
||||
(worker_id, event_type, qr_code_id, latitude, longitude, notes, timestamp)
|
||||
VALUES (?, "failed", ?, ?, ?, ?, CONVERT_TZ(NOW(), @@session.time_zone, '+00:00'))`,
|
||||
[userId, qrCodeValue, latitude, longitude, note]
|
||||
);
|
||||
return res.status(403).json({ message: `error.outsideGeofence|${distanceString}` });
|
||||
}
|
||||
}
|
||||
|
||||
// 3) QR Code and Status Validation (switch db -> conn; logic unchanged)
|
||||
if (qrCodeValue !== 'FORCE_CLOCK_OUT') {
|
||||
const [qrRows] = await conn.execute('SELECT is_active FROM qr_codes WHERE id = ?', [qrCodeValue]); // CHANGED
|
||||
if (qrRows.length === 0 || !qrRows[0].is_active) {
|
||||
return res.status(400).json({ message: 'error.invalidQrCode' });
|
||||
}
|
||||
}
|
||||
|
||||
const [lastEvent] = await conn.execute( // CHANGED
|
||||
'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 — store UTC via SQL conversion (no JS date math)
|
||||
await conn.execute(
|
||||
`INSERT INTO clock_records
|
||||
(worker_id, event_type, qr_code_id, latitude, longitude, timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, CONVERT_TZ(NOW(), @@session.time_zone, '+00:00'))`,
|
||||
[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 {
|
||||
if (conn) conn.release();
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/workers/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
|
||||
Reference in New Issue
Block a user