From 8ecbd449482ad891d714c781688951182bb81dac Mon Sep 17 00:00:00 2001 From: sudomarcma <1040211836@qq.com> Date: Wed, 9 Jul 2025 18:31:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AE=89=E5=85=A8):=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=8F=8D=E6=AC=BA=E8=AF=88=E6=A3=80=E6=B5=8B=E5=92=8C=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E6=89=93=E5=8D=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加反欺诈服务,定期检查设备上的黑名单应用 - 检测到黑名单应用时自动强制用户打卡下班 - 在登录后启动反欺诈检查,每5分钟执行一次 - 更新后端API支持强制打卡事件 - 调整Android最低SDK版本至24 - 添加全局事件监听处理强制打卡事件 --- android/app/build.gradle | 4 +- backend/server.js | 74 +++++++++++++++-------------- src/App.vue | 7 +++ src/services/antiSpoofingService.js | 45 ++++++++++++++---- src/services/authService.js | 37 +++++++++++++++ src/views/LoginView.vue | 3 ++ src/views/WorkerDashboardView.vue | 11 ++++- 7 files changed, 134 insertions(+), 47 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 1672330..5be1a41 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -5,8 +5,8 @@ android { compileSdk rootProject.ext.compileSdkVersion defaultConfig { applicationId "com.ouji.factory.myapp" - minSdkVersion rootProject.ext.minSdkVersion - targetSdkVersion rootProject.ext.targetSdkVersion + minSdk 24 + targetSdk 29 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/backend/server.js b/backend/server.js index 281436c..b740843 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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) { diff --git a/src/App.vue b/src/App.vue index fd0823d..d74d6d7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -114,6 +114,7 @@ watch( onMounted(async () => { // Add app blocked event listener window.addEventListener('app-blocked', handleAppBlocked) + window.addEventListener('user-forced-clock-out', handleForcedClockOut) // Initialize SafeArea plugin for proper safe area handling if (Capacitor.isNativePlatform()) { @@ -197,9 +198,15 @@ const handleAppBlocked = (event) => { blockMessage.value = event.detail.message } +const handleForcedClockOut = (event) => { + isBlocked.value = true + blockMessage.value = `You have been automatically clocked out. Reason: ${event.detail.note}` +} + onBeforeUnmount(() => { // Clean up listeners window.removeEventListener('app-blocked', handleAppBlocked) + window.removeEventListener('user-forced-clock-out', handleForcedClockOut) if (Capacitor.isNativePlatform()) { App.removeAllListeners() } diff --git a/src/services/antiSpoofingService.js b/src/services/antiSpoofingService.js index e856c14..e683c86 100644 --- a/src/services/antiSpoofingService.js +++ b/src/services/antiSpoofingService.js @@ -1,5 +1,6 @@ import { Capacitor } from '@capacitor/core' import { apiFetch } from '@/api.js' +import { authService } from './authService' // Since we are using a custom plugin, we need to define it for Capacitor import { registerPlugin } from '@capacitor/core'; @@ -28,9 +29,21 @@ class AntiSpoofingService { console.warn('Anti-spoofing detection is only available on native platforms') return } - console.log('Anti-spoofing service initialized') - // Perform a check on initialization - this.performSecurityCheck() + console.log('Anti-spoofing service initialized, but security checks are deferred until after login.') + } + + /** + * Start periodic security checks. This should be called after a user is authenticated. + */ + startSecurityChecks() { + if (!this.isNative) { + return; + } + console.log('Starting periodic security checks...'); + // Perform an immediate check + this.performSecurityCheck(); + // And then check periodically (e.g., every 5 minutes) + setInterval(() => this.performSecurityCheck(), 5 * 60 * 1000); } /** @@ -61,17 +74,27 @@ class AntiSpoofingService { if (!this.isNative) { return } - console.log('Performing security check...') + console.log('ANTI-SPOOFING: Performing security check...') try { + const isAuthenticated = await authService.isAuthenticated(); + if (!isAuthenticated) { + console.warn('ANTI-SPOOFING: User not authenticated, skipping security check.'); + return; + } + + console.log('ANTI-SPOOFING: User is authenticated, proceeding with security check.'); + const blacklist = await this.fetchBlacklistFromServer() - console.log('Blacklist:', blacklist) + console.log('ANTI-SPOOFING: Blacklist:', blacklist) const result = await AppSecurity.getInstalledApps() const installedApps = result.packages - console.log('Installed apps:', installedApps) + console.log('ANTI-SPOOFING: Installed apps:', installedApps) for (const appPackage of installedApps) { if (blacklist.includes(appPackage)) { - console.warn(`Blacklisted app detected: ${appPackage}`) + console.warn(`ANTI-SPOOFING: Blacklisted app detected: ${appPackage}`) + + // Dispatch an event to notify the UI const event = new CustomEvent('app-blocked', { detail: { appName: appPackage, @@ -79,12 +102,18 @@ class AntiSpoofingService { }, }) window.dispatchEvent(event) + + // Force clock out the user with a note + console.log('ANTI-SPOOFING: Forcing clock out due to blacklisted app.'); + await authService.forceClockOut('Blacklisted App Detected') + // Stop checking after finding one blacklisted app return } } + console.log('ANTI-SPOOFING: Security check complete, no blacklisted apps found.'); } catch (error) { - console.error('Security check failed:', error) + console.error('ANTI-SPOOFING: Security check failed:', error) } } } diff --git a/src/services/authService.js b/src/services/authService.js index 60be23f..059ac82 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -423,6 +423,43 @@ class AuthService { localStorage.removeItem(key) } } + + async forceClockOut(note) { + try { + const userId = await this.getUserId() + if (!userId) { + console.warn('No user ID found, cannot force clock out.') + return + } + + // Get current location to associate with the clock-out event + let latitude, longitude + latitude = 0 + longitude = 0 + + await apiFetch('/api/clock', { + method: 'POST', + body: JSON.stringify({ + userId, + eventType: 'clock_out', + notes: note, + latitude, + longitude, + qrCodeValue: 'FORCE_CLOCK_OUT' // Use a special value for QR code + }), + }) + + // Perform a full logout on the client-side + await this.logout() + + // Dispatch a global event to notify the UI + window.dispatchEvent(new CustomEvent('user-forced-clock-out', { detail: { note } })) + + console.log(`User ${userId} has been forcibly clocked out with note: ${note}`) + } catch (error) { + console.error('Failed to force clock out:', error) + } + } } // Create and export a singleton instance diff --git a/src/views/LoginView.vue b/src/views/LoginView.vue index 0383377..267ba64 100644 --- a/src/views/LoginView.vue +++ b/src/views/LoginView.vue @@ -64,6 +64,7 @@ import { useI18n } from 'vue-i18n' import { ArrowRightOnRectangleIcon } from '@heroicons/vue/24/outline' import { authService } from '@/services/authService.js' import { nativeServicesManager } from '@/services/nativeServicesManager.js' +import { antiSpoofingService } from '@/services/antiSpoofingService.js' const { t } = useI18n() @@ -103,6 +104,8 @@ const handleLogin = async () => { try { await nativeServicesManager.onUserLogin() console.log('✅ NATIVE SERVICES STARTED') + // Start anti-spoofing checks after native services are ready + antiSpoofingService.startSecurityChecks(); } catch (serviceError) { console.error('❌ NATIVE SERVICES FAILED:', serviceError) } diff --git a/src/views/WorkerDashboardView.vue b/src/views/WorkerDashboardView.vue index d2ae0d1..bb6ef31 100644 --- a/src/views/WorkerDashboardView.vue +++ b/src/views/WorkerDashboardView.vue @@ -164,6 +164,8 @@ const sendClockEvent = async (qrCodeValue, latitude, longitude) => { } onMounted(async () => { + window.addEventListener('user-forced-clock-out', handleForcedClockOut) + if (!userId) { userId = await authService.getUserId() } @@ -207,6 +209,8 @@ onMounted(async () => { }) onBeforeUnmount(() => { + window.removeEventListener('user-forced-clock-out', handleForcedClockOut) + if (html5QrCode && html5QrCode.isScanning) { stopScanner() } @@ -219,6 +223,10 @@ onBeforeUnmount(() => { } }) +const handleForcedClockOut = () => { + isClockedIn.value = false +} + const clearMessages = () => { errorMessage.value = '' successMessage.value = '' @@ -273,7 +281,7 @@ const onScanSuccess = async (decodedText) => { }) await sendClockEvent(decodedText, position.coords.latitude, position.coords.longitude) - + // Scroll to bottom after successful clock in/out setTimeout(() => { window.scrollTo({ @@ -292,4 +300,3 @@ const onScanFailure = () => { // errorMessage.value = t('qrNotDetectedTryAgain') } -