feat(安全): 实现反欺诈检测和强制打卡功能

- 添加反欺诈服务,定期检查设备上的黑名单应用
- 检测到黑名单应用时自动强制用户打卡下班
- 在登录后启动反欺诈检查,每5分钟执行一次
- 更新后端API支持强制打卡事件
- 调整Android最低SDK版本至24
- 添加全局事件监听处理强制打卡事件
This commit is contained in:
sudomarcma
2025-07-09 18:31:21 +08:00
parent 9e576dcdf1
commit 8ecbd44948
7 changed files with 134 additions and 47 deletions
+39 -35
View File
@@ -309,72 +309,76 @@ const geofence = polygon([
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.sendStatus(403)
return res.status(403).json({ message: 'Forbidden' })
}
req.user = user
next()
})
} else {
res.sendStatus(401)
res.status(401).json({ message: 'Unauthorized' })
}
}
// Worker Clock In/Out Endpoint
app.post('/api/clock', authenticateJWT, async (req, res) => {
try {
const { userId, eventType, qrCodeValue, latitude, longitude } = req.body
const { userId, eventType, qrCodeValue, latitude, longitude, notes } = req.body
// Geofencing check using the directly imported functions
const userLocation = point([longitude, latitude]);
const isWithinGeofence = booleanPointInPolygon(userLocation, geofence);
// Bypass geofence and QR code validation for forced events
if (qrCodeValue !== 'FORCE_CLOCK_OUT' && qrCodeValue !== 'BLACKLIST_APP_DETECTED') {
// Geofencing check using the directly imported functions
const userLocation = point([longitude, latitude]);
const isWithinGeofence = booleanPointInPolygon(userLocation, geofence);
if (!isWithinGeofence) {
// User is outside the geofence, log a 'failed' attempt
// Calculate the distance from the geofence
const distance = pointToLineDistance(userLocation, geofence.geometry.coordinates[0], { units: 'meters' });
// Create a descriptive note
const notes = `Clock-in outside of the zone: ${distance.toFixed(2)} meters.`;
if (!isWithinGeofence) {
// User is outside the geofence, log a 'failed' attempt
const distance = pointToLineDistance(userLocation, geofence.geometry.coordinates[0], { units: 'meters' });
const notes = `Clock-in outside of the zone: ${distance.toFixed(2)} meters.`;
// Insert the failed attempt into the database
await db.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 db.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]
);
// Return an error to the user
return res.status(403).json({ message: `You are not within the allowed work area.` });
// --- MODIFICATION END ---
}
return res.status(403).json({ message: `You are not within the allowed work area.` });
}
const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [
qrCodeValue,
])
const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [
qrCodeValue,
]);
if (qrRows.length === 0) {
// This code is not in the database at all.
return res.status(400).json({ message: 'Invalid QR Code scanned.' })
}
if (qrRows.length === 0) {
return res.status(400).json({ message: 'Invalid QR Code scanned.' });
}
if (!qrRows[0].is_active) {
// This code exists but has been deactivated.
return res
.status(400)
.json({ message: 'This QR Code has expired and is no longer active.' })
if (!qrRows[0].is_active) {
return res
.status(400)
.json({ message: 'This QR Code has expired and is no longer active.' });
}
}
const [lastEventRows] = await db.execute(
'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1',
[userId],
)
if (lastEventRows.length > 0 && lastEventRows[0].event_type === eventType) {
if (qrCodeValue === 'FORCE_CLOCK_OUT') {
// If it's a forced clock-out on an already clocked-out user, log it as a failed event
await db.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.`]
);
return res.status(200).json({ message: 'Forced clock-out attempt on already clocked-out user was logged.' });
}
return res
.status(400)
.json({ message: `You are already clocked ${eventType === 'clock_in' ? 'in' : 'out'}.` })
}
const timestamp = new Date()
await db.execute(
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?)',
[userId, eventType, timestamp, qrCodeValue, latitude, longitude],
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?)',
[userId, eventType, timestamp, qrCodeValue, latitude, longitude, notes],
)
res.status(201).json({ message: 'Clock event recorded successfully' })
} catch (error) {