docs: 添加项目文档和优化后台服务
This commit is contained in:
+74
-53
@@ -320,70 +320,88 @@ const geofence = polygon([
|
||||
}
|
||||
}
|
||||
|
||||
// Worker Clock In/Out Endpoint
|
||||
// Worker Clock In/Out Endpoint - Optimized Version
|
||||
app.post('/api/clock', authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const { userId, eventType, qrCodeValue, latitude, longitude, notes } = req.body
|
||||
const { userId, eventType, qrCodeValue, latitude, longitude, notes } = req.body;
|
||||
const connection = await db.getConnection(); // Get connection from pool first
|
||||
|
||||
// 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);
|
||||
try {
|
||||
// Start transaction
|
||||
await connection.beginTransaction();
|
||||
|
||||
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.`;
|
||||
// Bypass checks for special cases
|
||||
if (qrCodeValue !== 'FORCE_CLOCK_OUT') {
|
||||
// Parallelize geofence and QR code checks
|
||||
const [geofenceCheck, qrCheck] = await Promise.all([
|
||||
booleanPointInPolygon(point([longitude, latitude]), geofence),
|
||||
connection.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [qrCodeValue])
|
||||
]);
|
||||
|
||||
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]
|
||||
);
|
||||
if (!geofenceCheck) {
|
||||
const distance = pointToLineDistance(point([longitude, latitude]),
|
||||
geofence.geometry.coordinates[0], { units: 'meters' });
|
||||
const notes = `Clock-in outside of the zone: ${distance.toFixed(2)} meters.`;
|
||||
|
||||
return res.status(403).json({ message: `You are not within the allowed work area.` });
|
||||
await connection.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 connection.commit();
|
||||
return res.status(403).json({ message: `You are not within the allowed work area.` });
|
||||
}
|
||||
|
||||
if (qrCheck[0].length === 0) {
|
||||
await connection.rollback();
|
||||
return res.status(400).json({ message: 'Invalid QR Code scanned.' });
|
||||
}
|
||||
|
||||
if (!qrCheck[0][0].is_active) {
|
||||
await connection.rollback();
|
||||
return res.status(400).json({ message: 'This QR Code has expired and is no longer active.' });
|
||||
}
|
||||
}
|
||||
|
||||
const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [
|
||||
qrCodeValue,
|
||||
]);
|
||||
// Check last event
|
||||
const [lastEvent] = await connection.execute(
|
||||
'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (qrRows.length === 0) {
|
||||
return res.status(400).json({ message: 'Invalid QR Code scanned.' });
|
||||
}
|
||||
|
||||
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'}.` })
|
||||
// If it's a forced clock-out, log it as a failed event regardless of previous state
|
||||
await connection.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.`]
|
||||
);
|
||||
await connection.commit();
|
||||
return res.status(200).json({ message: 'Forced clock-out attempt was logged.' });
|
||||
}
|
||||
|
||||
if (lastEvent.length > 0 && lastEvent[0].event_type === eventType) {
|
||||
await connection.rollback();
|
||||
return res.status(400).json({ message: `You are already clocked ${eventType === 'clock_in' ? 'in' : 'out'}.` });
|
||||
}
|
||||
|
||||
// Insert new record
|
||||
const timestamp = new Date();
|
||||
await connection.execute(
|
||||
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[userId, eventType, timestamp, qrCodeValue || null, latitude || null, longitude || null, notes || null]
|
||||
);
|
||||
|
||||
await connection.commit();
|
||||
res.status(201).json({ message: 'Clock event recorded successfully' });
|
||||
} catch (err) {
|
||||
await connection.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
connection.release();
|
||||
}
|
||||
const timestamp = new Date()
|
||||
await db.execute(
|
||||
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
[userId, eventType, timestamp, qrCodeValue || null, latitude || null, longitude || null, notes || null],
|
||||
)
|
||||
res.status(201).json({ message: 'Clock event recorded successfully' })
|
||||
} catch (error) {
|
||||
console.error('Clock event error:', error)
|
||||
res.status(500).json({ message: 'Database error during clock event.' })
|
||||
console.error('Clock event error:', error);
|
||||
res.status(500).json({ message: 'Database error during clock event.' });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1006,7 +1024,9 @@ const geofence = polygon([
|
||||
})
|
||||
|
||||
|
||||
// Security Check Endpoint
|
||||
// Security Check Endpoint - COMMENTED OUT FOR SERVER-SIDE SECURITY PREFERENCE
|
||||
// Client-side security computation removed per user preference for server-side security
|
||||
/*
|
||||
app.post('/api/security/check', authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const { userId, timestamp, deviceInfo, securityCheck } = req.body
|
||||
@@ -1077,6 +1097,7 @@ const geofence = polygon([
|
||||
res.status(500).json({ message: 'Database error during security check.' })
|
||||
}
|
||||
})
|
||||
*/
|
||||
|
||||
// Get Security Status Endpoint
|
||||
app.get('/api/security/status/:userId', authenticateJWT, async (req, res) => {
|
||||
|
||||
@@ -0,0 +1,708 @@
|
||||
# Android Implementation Details - Nilai Clock Client
|
||||
|
||||
## Overview
|
||||
|
||||
This document provides comprehensive details about the current Android implementation of the Nilai Clock Client, including native plugins, services, permissions, and platform-specific configurations.
|
||||
|
||||
## Android Project Structure
|
||||
|
||||
### Capacitor Android Project Layout
|
||||
```
|
||||
android/
|
||||
├── app/
|
||||
│ ├── src/main/
|
||||
│ │ ├── java/com/ouji/factory/myapp/
|
||||
│ │ │ ├── MainActivity.java
|
||||
│ │ │ └── AppSecurity.java
|
||||
│ │ ├── res/
|
||||
│ │ │ ├── values/
|
||||
│ │ │ ├── xml/
|
||||
│ │ │ └── drawable/
|
||||
│ │ └── AndroidManifest.xml
|
||||
│ ├── build.gradle
|
||||
│ └── capacitor.settings.gradle
|
||||
├── build.gradle
|
||||
├── gradle.properties
|
||||
└── settings.gradle
|
||||
```
|
||||
|
||||
### Key Android Files
|
||||
|
||||
#### MainActivity.java
|
||||
```java
|
||||
package com.ouji.factory.myapp;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
import com.getcapacitor.Plugin;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
// Register custom AppSecurity plugin
|
||||
registerPlugin(AppSecurity.class);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Configure native window behavior
|
||||
setupNativeWindow();
|
||||
}
|
||||
|
||||
private void setupNativeWindow() {
|
||||
// Hide system UI for immersive experience
|
||||
getWindow().setFlags(
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
|
||||
);
|
||||
|
||||
// Configure status bar
|
||||
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||
WindowInsetsControllerCompat controller =
|
||||
WindowCompat.getInsetsController(getWindow(), getWindow().getDecorView());
|
||||
|
||||
if (controller != null) {
|
||||
controller.setSystemBarsBehavior(
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### AppSecurity.java (Custom Plugin)
|
||||
```java
|
||||
package com.ouji.factory.myapp;
|
||||
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
import org.json.JSONArray;
|
||||
import java.util.List;
|
||||
|
||||
@CapacitorPlugin(name = "AppSecurity")
|
||||
public class AppSecurity extends Plugin {
|
||||
|
||||
@PluginMethod
|
||||
public void getInstalledApps(PluginCall call) {
|
||||
PackageManager pm = getContext().getPackageManager();
|
||||
List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA);
|
||||
JSONArray appPackages = new JSONArray();
|
||||
|
||||
for (ApplicationInfo packageInfo : packages) {
|
||||
appPackages.put(packageInfo.packageName);
|
||||
}
|
||||
|
||||
JSObject ret = new JSObject();
|
||||
ret.put("packages", appPackages);
|
||||
call.resolve(ret);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void checkRootStatus(PluginCall call) {
|
||||
boolean isRooted = detectRoot();
|
||||
JSObject ret = new JSObject();
|
||||
ret.put("isRooted", isRooted);
|
||||
ret.put("riskLevel", isRooted ? "high" : "low");
|
||||
call.resolve(ret);
|
||||
}
|
||||
|
||||
private boolean detectRoot() {
|
||||
// Check for common root indicators
|
||||
String[] rootPaths = {
|
||||
"/system/app/Superuser.apk",
|
||||
"/sbin/su",
|
||||
"/system/bin/su",
|
||||
"/system/xbin/su",
|
||||
"/data/local/xbin/su",
|
||||
"/data/local/bin/su",
|
||||
"/system/sd/xbin/su",
|
||||
"/system/bin/failsafe/su",
|
||||
"/data/local/su"
|
||||
};
|
||||
|
||||
for (String path : rootPaths) {
|
||||
if (new java.io.File(path).exists()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Android Permissions
|
||||
|
||||
### AndroidManifest.xml Permissions
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- Background location for Android 10+ -->
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
|
||||
<!-- Prevent app from being killed -->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
```
|
||||
|
||||
### Permission Handling Strategy
|
||||
|
||||
#### Location Permissions
|
||||
1. **ACCESS_FINE_LOCATION**: Required for precise location
|
||||
2. **ACCESS_BACKGROUND_LOCATION**: Required for background tracking
|
||||
3. **Runtime Permission Flow**:
|
||||
- Request fine location first
|
||||
- If granted, request background location
|
||||
- Handle permission denial gracefully
|
||||
|
||||
#### Camera Permission
|
||||
- **CAMERA**: Required for QR code scanning
|
||||
- **Runtime Request**: Requested when QR scanner is first used
|
||||
- **Fallback**: File upload option if camera denied
|
||||
|
||||
## Background Location Implementation
|
||||
|
||||
### Android Background Geolocation Service
|
||||
|
||||
#### Service Configuration
|
||||
```javascript
|
||||
// Background geolocation watcher configuration
|
||||
const androidConfig = {
|
||||
requestPermissions: true,
|
||||
stale: false,
|
||||
distanceFilter: 50, // Minimum 50 meters movement
|
||||
backgroundMessage: "Tracking work location for attendance verification",
|
||||
backgroundTitle: "Nilai Clock - Location Tracking",
|
||||
// Android-specific options
|
||||
enableHighAccuracy: true,
|
||||
interval: 30000, // 30 seconds
|
||||
fastestInterval: 15000, // 15 seconds minimum
|
||||
maxWaitTime: 60000 // 1 minute maximum wait
|
||||
}
|
||||
```
|
||||
|
||||
#### Foreground Service Notification
|
||||
```xml
|
||||
<!-- Required for Android 8+ background location -->
|
||||
<service android:name="com.transistorsoft.capacitor.backgroundgeolocation.BackgroundGeolocationService"
|
||||
android:foregroundServiceType="location"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
```
|
||||
|
||||
#### Location Update Handling
|
||||
```javascript
|
||||
// Handle location updates from background service
|
||||
const handleLocationUpdate = (location) => {
|
||||
console.log('Android location update:', {
|
||||
latitude: location.coords.latitude,
|
||||
longitude: location.coords.longitude,
|
||||
accuracy: location.coords.accuracy,
|
||||
timestamp: location.timestamp,
|
||||
source: 'background'
|
||||
})
|
||||
|
||||
// Send to server if user is clocked in
|
||||
if (this.isClockedIn) {
|
||||
this.sendLocationToServer(location, { source: 'background' })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Battery Optimization Handling
|
||||
|
||||
#### Request Battery Optimization Exemption
|
||||
```java
|
||||
// In MainActivity.java - Request to ignore battery optimization
|
||||
private void requestBatteryOptimizationExemption() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Intent intent = new Intent();
|
||||
String packageName = getPackageName();
|
||||
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
|
||||
|
||||
if (!pm.isIgnoringBatteryOptimizations(packageName)) {
|
||||
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + packageName));
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Network Security Configuration
|
||||
|
||||
### network_security_config.xml
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<!-- Allow cleartext traffic for development -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
<domain includeSubdomains="true">10.0.2.2</domain>
|
||||
<domain includeSubdomains="true">198.18.0.1</domain>
|
||||
<domain includeSubdomains="true">192.168.36.54</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="user" />
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
|
||||
<!-- Debug overrides for development -->
|
||||
<debug-overrides>
|
||||
<trust-anchors>
|
||||
<certificates src="user"/>
|
||||
<certificates src="system"/>
|
||||
</trust-anchors>
|
||||
</debug-overrides>
|
||||
|
||||
<!-- Production configuration -->
|
||||
<domain-config>
|
||||
<domain includeSubdomains="true">myapp.ouji.com</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="system"/>
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
```
|
||||
|
||||
### Application Security Configuration
|
||||
```xml
|
||||
<!-- In AndroidManifest.xml -->
|
||||
<application
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:allowBackup="false"
|
||||
android:extractNativeLibs="false">
|
||||
```
|
||||
|
||||
## Device UUID Generation
|
||||
|
||||
### Android-Specific Implementation
|
||||
```javascript
|
||||
// Android device UUID generation strategy
|
||||
async generateAndroidDeviceUuid() {
|
||||
try {
|
||||
const deviceInfo = await Device.getInfo()
|
||||
const deviceId = await Device.getId()
|
||||
|
||||
// Create unique fingerprint from device characteristics
|
||||
const baseString = [
|
||||
deviceInfo.platform, // "android"
|
||||
deviceInfo.model, // "Pixel 6"
|
||||
deviceInfo.manufacturer, // "Google"
|
||||
deviceId.identifier, // Android ID or UUID
|
||||
deviceInfo.osVersion, // "13"
|
||||
await this.getAndroidSpecificId()
|
||||
].join('-')
|
||||
|
||||
// Hash the base string for consistent UUID
|
||||
return await this.hashString(baseString)
|
||||
} catch (error) {
|
||||
console.error('Android UUID generation failed:', error)
|
||||
return this.generateFallbackUuid()
|
||||
}
|
||||
}
|
||||
|
||||
async getAndroidSpecificId() {
|
||||
try {
|
||||
// Try to get Android ID or generate persistent identifier
|
||||
const { value } = await Preferences.get({ key: 'android_persistent_id' })
|
||||
|
||||
if (value) return value
|
||||
|
||||
// Generate new persistent ID for this installation
|
||||
const persistentId = this.generateRandomId(32)
|
||||
await Preferences.set({
|
||||
key: 'android_persistent_id',
|
||||
value: persistentId
|
||||
})
|
||||
|
||||
return persistentId
|
||||
} catch (error) {
|
||||
return `android-fallback-${Date.now()}`
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-Spoofing Implementation
|
||||
|
||||
### GPS Spoofing Detection
|
||||
```javascript
|
||||
// Android-specific anti-spoofing checks
|
||||
async performAndroidSecurityCheck() {
|
||||
try {
|
||||
// Get installed applications
|
||||
const installedApps = await AppSecurity.getInstalledApps()
|
||||
|
||||
// Check against blacklist
|
||||
const blacklist = await this.fetchBlacklistFromServer()
|
||||
const suspiciousApps = installedApps.packages.filter(pkg =>
|
||||
blacklist.includes(pkg)
|
||||
)
|
||||
|
||||
// Check for root access
|
||||
const rootStatus = await AppSecurity.checkRootStatus()
|
||||
|
||||
// Determine risk level
|
||||
let riskLevel = 'low'
|
||||
let riskFactors = []
|
||||
|
||||
if (suspiciousApps.length > 0) {
|
||||
riskLevel = 'high'
|
||||
riskFactors.push('gps_spoofing_apps_detected')
|
||||
}
|
||||
|
||||
if (rootStatus.isRooted) {
|
||||
riskLevel = 'high'
|
||||
riskFactors.push('device_rooted')
|
||||
}
|
||||
|
||||
const result = {
|
||||
platform: 'android',
|
||||
riskLevel,
|
||||
riskFactors,
|
||||
suspiciousApps,
|
||||
isRooted: rootStatus.isRooted,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
|
||||
// Send to server
|
||||
await this.sendSecurityCheckToServer(result)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Android security check failed:', error)
|
||||
return { riskLevel: 'unknown', error: error.message }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Known GPS Spoofing Apps Blacklist
|
||||
```javascript
|
||||
const gpsSpooferBlacklist = [
|
||||
// Popular GPS spoofing apps
|
||||
'com.lexa.fakegps',
|
||||
'com.incorporateapps.fakegps.fre',
|
||||
'com.blogspot.newapphorizons.fakegps',
|
||||
'com.theappninjas.gpsjoystick',
|
||||
'com.fakegps.mock',
|
||||
'com.mock.location.app',
|
||||
'com.gpsemulator',
|
||||
'com.locationspoofer',
|
||||
'com.fakegps.location',
|
||||
'com.mock.gps.location',
|
||||
|
||||
// Developer tools that can spoof location
|
||||
'com.android.development',
|
||||
'com.android.development_settings',
|
||||
'com.mock.location.developer',
|
||||
|
||||
// Xposed modules
|
||||
'de.robv.android.xposed.installer',
|
||||
'com.solohsu.android.edxp.manager',
|
||||
|
||||
// Root management apps
|
||||
'com.noshufou.android.su',
|
||||
'com.noshufou.android.su.elite',
|
||||
'eu.chainfire.supersu',
|
||||
'com.koushikdutta.superuser',
|
||||
'com.thirdparty.superuser',
|
||||
'com.yellowes.su'
|
||||
]
|
||||
```
|
||||
|
||||
## QR Code Scanning Implementation
|
||||
|
||||
### Camera Integration
|
||||
```javascript
|
||||
// Android camera configuration for QR scanning
|
||||
const androidCameraConfig = {
|
||||
fps: 10,
|
||||
qrbox: { width: 250, height: 250 },
|
||||
aspectRatio: 1.0,
|
||||
disableFlip: false,
|
||||
videoConstraints: {
|
||||
facingMode: 'environment', // Back camera
|
||||
advanced: [{
|
||||
focusMode: 'continuous',
|
||||
zoom: 1.0
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize QR scanner
|
||||
const initializeAndroidQRScanner = () => {
|
||||
html5QrCode = new Html5Qrcode('qr-reader')
|
||||
|
||||
html5QrCode.start(
|
||||
{ facingMode: 'environment' },
|
||||
androidCameraConfig,
|
||||
onScanSuccess,
|
||||
onScanFailure
|
||||
).catch(err => {
|
||||
console.error('Android QR scanner failed to start:', err)
|
||||
// Fallback to file upload
|
||||
showFileUploadOption()
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Camera Permission Handling
|
||||
```javascript
|
||||
// Check and request camera permission
|
||||
const checkAndroidCameraPermission = async () => {
|
||||
try {
|
||||
const permissions = await Camera.checkPermissions()
|
||||
|
||||
if (permissions.camera === 'granted') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (permissions.camera === 'denied') {
|
||||
// Guide user to settings
|
||||
showPermissionDeniedAlert()
|
||||
return false
|
||||
}
|
||||
|
||||
// Request permission
|
||||
const result = await Camera.requestPermissions()
|
||||
return result.camera === 'granted'
|
||||
} catch (error) {
|
||||
console.error('Camera permission check failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Local Storage and Preferences
|
||||
|
||||
### Android Secure Storage
|
||||
```javascript
|
||||
// Android uses EncryptedSharedPreferences
|
||||
class AndroidSecureStorage {
|
||||
async setItem(key, value) {
|
||||
try {
|
||||
await Preferences.set({
|
||||
key: key,
|
||||
value: value
|
||||
})
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Android secure storage set failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async getItem(key) {
|
||||
try {
|
||||
const { value } = await Preferences.get({ key })
|
||||
return value
|
||||
} catch (error) {
|
||||
console.error('Android secure storage get failed:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async removeItem(key) {
|
||||
try {
|
||||
await Preferences.remove({ key })
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Android secure storage remove failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Optimizations
|
||||
|
||||
### Memory Management
|
||||
```javascript
|
||||
// Android-specific memory optimization
|
||||
const optimizeAndroidPerformance = () => {
|
||||
// Limit location update frequency
|
||||
const locationThrottle = 30000 // 30 seconds
|
||||
|
||||
// Clean up unused resources
|
||||
const cleanupResources = () => {
|
||||
if (html5QrCode && !html5QrCode.isScanning) {
|
||||
html5QrCode.clear()
|
||||
}
|
||||
|
||||
// Clear old location data
|
||||
if (locationHistory.length > 100) {
|
||||
locationHistory.splice(0, locationHistory.length - 50)
|
||||
}
|
||||
}
|
||||
|
||||
// Set up periodic cleanup
|
||||
setInterval(cleanupResources, 300000) // 5 minutes
|
||||
}
|
||||
```
|
||||
|
||||
### Battery Usage Optimization
|
||||
```javascript
|
||||
// Optimize for Android battery usage
|
||||
const optimizeAndroidBattery = () => {
|
||||
// Reduce location accuracy when not critical
|
||||
const adaptiveLocationConfig = {
|
||||
enableHighAccuracy: false, // Use network location when possible
|
||||
timeout: 30000,
|
||||
maximumAge: 60000 // Accept 1-minute old location
|
||||
}
|
||||
|
||||
// Pause non-critical background tasks
|
||||
const pauseNonCriticalTasks = () => {
|
||||
// Stop security checks when app is backgrounded
|
||||
if (document.hidden) {
|
||||
clearInterval(securityCheckInterval)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('visibilitychange', pauseNonCriticalTasks)
|
||||
}
|
||||
```
|
||||
|
||||
## Build Configuration
|
||||
|
||||
### Android Build Variants
|
||||
```gradle
|
||||
// app/build.gradle
|
||||
android {
|
||||
buildTypes {
|
||||
debug {
|
||||
debuggable true
|
||||
minifyEnabled false
|
||||
applicationIdSuffix ".debug"
|
||||
}
|
||||
release {
|
||||
debuggable false
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions "environment"
|
||||
productFlavors {
|
||||
development {
|
||||
dimension "environment"
|
||||
applicationIdSuffix ".dev"
|
||||
buildConfigField "String", "API_BASE_URL", '"http://192.168.36.54:3000"'
|
||||
}
|
||||
production {
|
||||
dimension "environment"
|
||||
buildConfigField "String", "API_BASE_URL", '"https://myapp.ouji.com/nilai_clock_api"'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ProGuard Configuration
|
||||
```proguard
|
||||
# proguard-rules.pro
|
||||
-keep class com.ouji.factory.myapp.** { *; }
|
||||
-keep class com.getcapacitor.** { *; }
|
||||
-keep class com.transistorsoft.** { *; }
|
||||
|
||||
# Keep location service classes
|
||||
-keep class * extends android.app.Service
|
||||
-keep class * implements android.location.LocationListener
|
||||
|
||||
# Keep JSON serialization
|
||||
-keepattributes Signature
|
||||
-keepattributes *Annotation*
|
||||
-keep class com.google.gson.** { *; }
|
||||
```
|
||||
|
||||
## Debugging and Logging
|
||||
|
||||
### Android Debug Configuration
|
||||
```javascript
|
||||
// Android-specific debug logging
|
||||
const AndroidDebugger = {
|
||||
enableLocationLogging: () => {
|
||||
console.log('Android location debugging enabled')
|
||||
// Log all location updates
|
||||
window.addEventListener('location-update', (event) => {
|
||||
console.log('Android location:', event.detail)
|
||||
})
|
||||
},
|
||||
|
||||
enableSecurityLogging: () => {
|
||||
console.log('Android security debugging enabled')
|
||||
// Log security check results
|
||||
window.addEventListener('security-check', (event) => {
|
||||
console.log('Android security:', event.detail)
|
||||
})
|
||||
},
|
||||
|
||||
logDeviceInfo: async () => {
|
||||
const deviceInfo = await Device.getInfo()
|
||||
console.log('Android device info:', deviceInfo)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
```javascript
|
||||
// Monitor Android app performance
|
||||
const AndroidPerformanceMonitor = {
|
||||
trackMemoryUsage: () => {
|
||||
if (performance.memory) {
|
||||
console.log('Memory usage:', {
|
||||
used: performance.memory.usedJSHeapSize,
|
||||
total: performance.memory.totalJSHeapSize,
|
||||
limit: performance.memory.jsHeapSizeLimit
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
trackLocationPerformance: () => {
|
||||
const startTime = performance.now()
|
||||
|
||||
return {
|
||||
end: () => {
|
||||
const endTime = performance.now()
|
||||
console.log(`Location operation took ${endTime - startTime} ms`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting Common Android Issues
|
||||
|
||||
### Location Issues
|
||||
1. **Background location stops**: Check battery optimization settings
|
||||
2. **Inaccurate location**: Verify GPS is enabled, check for spoofing apps
|
||||
3. **Permission denied**: Guide user through settings, explain necessity
|
||||
|
||||
### Camera Issues
|
||||
1. **Camera won't open**: Check permission, verify camera availability
|
||||
2. **QR scan fails**: Improve lighting, check QR code quality
|
||||
3. **App crashes on scan**: Handle camera resource conflicts
|
||||
|
||||
### Performance Issues
|
||||
1. **High battery usage**: Optimize location frequency, reduce background tasks
|
||||
2. **Memory leaks**: Properly dispose of resources, limit data retention
|
||||
3. **Slow startup**: Optimize initialization sequence, lazy load components
|
||||
|
||||
### Security Issues
|
||||
1. **False positives**: Refine blacklist, improve detection algorithms
|
||||
2. **Bypass attempts**: Enhance root detection, monitor for new spoofing methods
|
||||
3. **Data security**: Ensure proper encryption, secure API communication
|
||||
@@ -0,0 +1,547 @@
|
||||
# Nilai Clock Client - API Reference
|
||||
|
||||
## Base Configuration
|
||||
|
||||
### Production API Endpoint
|
||||
```
|
||||
https://myapp.ouji.com/nilai_clock_api/
|
||||
```
|
||||
|
||||
### Authentication
|
||||
All protected endpoints require JWT Bearer token in the Authorization header:
|
||||
```
|
||||
Authorization: Bearer <jwt-token>
|
||||
```
|
||||
|
||||
### Common Headers
|
||||
```
|
||||
Content-Type: application/json
|
||||
ngrok-skip-browser-warning: true
|
||||
```
|
||||
|
||||
## Authentication Endpoints
|
||||
|
||||
### POST `/api/auth/login`
|
||||
Authenticate user with username, password, and device UUID.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"username": "string",
|
||||
"password": "string",
|
||||
"deviceUuid": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"fullName": "Worker Full Name"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400`: Missing required fields
|
||||
- `401`: Invalid credentials
|
||||
- `403`: Device not authorized
|
||||
|
||||
**JWT Token Payload:**
|
||||
```json
|
||||
{
|
||||
"userId": 123,
|
||||
"role": "worker",
|
||||
"iat": 1642234567,
|
||||
"exp": 1642320967
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/api/auth/refresh`
|
||||
Refresh expired JWT token.
|
||||
|
||||
**Headers:** `Authorization: Bearer <current-token>`
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `401`: Invalid or expired token
|
||||
- `500`: Token refresh failed
|
||||
|
||||
## Worker Endpoints
|
||||
|
||||
### POST `/api/clock`
|
||||
Record clock in/out event with location verification.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"userId": 123,
|
||||
"eventType": "clock_in", // or "clock_out"
|
||||
"qrCodeValue": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"latitude": -6.2088,
|
||||
"longitude": 106.8456,
|
||||
"notes": "Optional notes"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"message": "Clock in successful",
|
||||
"timestamp": "2024-01-15T08:00:00.000Z",
|
||||
"eventType": "clock_in",
|
||||
"location": {
|
||||
"latitude": -6.2088,
|
||||
"longitude": 106.8456
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400`: Invalid event type or missing data
|
||||
- `401`: Unauthorized
|
||||
- `403`: Outside geofence area
|
||||
- `404`: QR code not found or inactive
|
||||
- `409`: Cannot clock in/out (already in same state)
|
||||
|
||||
**Special Cases:**
|
||||
- `qrCodeValue: "FORCE_CLOCK_OUT"` - Bypasses all validation checks
|
||||
|
||||
### GET `/api/worker/clock-history/:userId`
|
||||
Retrieve worker's clock in/out history.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Parameters:**
|
||||
- `userId` (path): Worker's user ID
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"event_type": "clock_in",
|
||||
"timestamp": "2024-01-15T08:00:00.000Z",
|
||||
"qrCodeUsedName": "Main Entrance"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"event_type": "clock_out",
|
||||
"timestamp": "2024-01-15T17:00:00.000Z",
|
||||
"qrCodeUsedName": "Main Entrance"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `401`: Unauthorized
|
||||
- `404`: Worker not found
|
||||
- `500`: Database error
|
||||
|
||||
### GET `/api/workers/:id`
|
||||
Get worker details by ID.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Parameters:**
|
||||
- `id` (path): Worker's user ID
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"full_name": "John Doe"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `401`: Unauthorized
|
||||
- `404`: Worker not found
|
||||
- `500`: Database error
|
||||
|
||||
## Location & Tracking Endpoints
|
||||
|
||||
### POST `/api/location/update`
|
||||
Send location update to server (background tracking).
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"userId": 123,
|
||||
"longitude": 106.8456,
|
||||
"latitude": -6.2088,
|
||||
"timestamp": "2024-01-15T08:00:00.000Z",
|
||||
"accuracy": 5.0,
|
||||
"source": "background" // or "manual", "clock_event"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"message": "Location updated successfully",
|
||||
"timestamp": "2024-01-15T08:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400`: Invalid location data
|
||||
- `401`: Unauthorized
|
||||
- `500`: Database error
|
||||
|
||||
### GET `/api/location/status/:userId`
|
||||
Get current location tracking status for user.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Parameters:**
|
||||
- `userId` (path): Worker's user ID
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"isTracking": true,
|
||||
"lastUpdate": "2024-01-15T08:00:00.000Z",
|
||||
"currentLocation": {
|
||||
"latitude": -6.2088,
|
||||
"longitude": 106.8456
|
||||
},
|
||||
"withinGeofence": true
|
||||
}
|
||||
```
|
||||
|
||||
## Security Endpoints
|
||||
|
||||
### POST `/api/security/check` - DEPRECATED
|
||||
**DEPRECATED:** This endpoint has been commented out in favor of server-side security computation.
|
||||
|
||||
Client-side security calculation has been removed per user preference for server-side security validation. Anti-spoofing functionality (fake GPS detection, UUID validation) continues to work through existing server-side mechanisms.
|
||||
|
||||
### GET `/api/security/app-blacklist`
|
||||
Get list of blacklisted applications.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
[
|
||||
"com.lexa.fakegps",
|
||||
"com.incorporateapps.fakegps.fre",
|
||||
"com.blogspot.newapphorizons.fakegps",
|
||||
"com.theappninjas.gpsjoystick"
|
||||
]
|
||||
```
|
||||
|
||||
### POST `/api/security/alert` - NOT IMPLEMENTED
|
||||
**NOT IMPLEMENTED:** This endpoint was documented but never implemented in the server.
|
||||
|
||||
Security alerts are handled server-side through existing mechanisms:
|
||||
- Geofence violations are logged automatically during location updates
|
||||
- Device validation failures are logged during login attempts
|
||||
- High-risk device detection is handled by server-side security validation
|
||||
|
||||
### GET `/api/security/status/:userId`
|
||||
Get security status for user.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Parameters:**
|
||||
- `userId` (path): Worker's user ID
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"latestSecurityCheck": {
|
||||
"id": 100,
|
||||
"user_id": 123,
|
||||
"timestamp": "2024-01-15T08:00:00.000Z",
|
||||
"risk_level": "low",
|
||||
"risk_score": 10
|
||||
},
|
||||
"recentAlerts": [
|
||||
{
|
||||
"id": 456,
|
||||
"alert_type": "location_anomaly",
|
||||
"severity": "medium",
|
||||
"created_at": "2024-01-15T07:30:00.000Z"
|
||||
}
|
||||
],
|
||||
"securityStatus": "low"
|
||||
}
|
||||
```
|
||||
|
||||
## Device Management Endpoints
|
||||
|
||||
### POST `/api/device/register`
|
||||
Register device for user account.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"userId": 123,
|
||||
"deviceUuid": "android-550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"message": "Device registered successfully",
|
||||
"deviceUuid": "android-550e8400-e29b-41d4-a716-446655440000",
|
||||
"registeredAt": "2024-01-15T08:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400`: Invalid device UUID
|
||||
- `401`: Unauthorized
|
||||
- `409`: Device already registered to another user
|
||||
- `500`: Database error
|
||||
|
||||
### POST `/api/device/validate`
|
||||
Validate device for user account.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"userId": 123,
|
||||
"deviceUuid": "android-550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"message": "Device validated successfully",
|
||||
"deviceInfo": {
|
||||
"registeredAt": "2024-01-15T08:00:00.000Z",
|
||||
"lastSeen": "2024-01-15T08:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
- `400`: Invalid device UUID
|
||||
- `401`: Unauthorized
|
||||
- `403`: Device not authorized for this user
|
||||
- `404`: Device not found
|
||||
- `500`: Database error
|
||||
|
||||
**Note:** Managers bypass device validation and always receive `valid: true`.
|
||||
|
||||
## Manager Endpoints (Web Only)
|
||||
|
||||
### GET `/api/managers/workers`
|
||||
Get list of all workers with filtering and pagination.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Query Parameters:**
|
||||
- `search` (optional): Search term for worker names
|
||||
- `page` (optional): Page number (default: 1)
|
||||
- `limit` (optional): Items per page (default: 20)
|
||||
- `tags` (optional): Comma-separated tag IDs
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
{
|
||||
"workers": [
|
||||
{
|
||||
"id": 123,
|
||||
"username": "worker123",
|
||||
"full_name": "John Doe",
|
||||
"created_at": "2024-01-01T00:00:00.000Z",
|
||||
"tags": "Production, Day Shift"
|
||||
}
|
||||
],
|
||||
"totalCount": 50,
|
||||
"currentPage": 1,
|
||||
"totalPages": 3
|
||||
}
|
||||
```
|
||||
|
||||
### GET `/api/managers/qr-codes`
|
||||
Get list of all QR codes.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Success Response (200):**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Main Entrance",
|
||||
"is_active": true,
|
||||
"created_at": "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### POST `/api/managers/qr-codes`
|
||||
Create new QR code.
|
||||
|
||||
**Headers:** `Authorization: Bearer <token>`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Side Entrance"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (201):**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"name": "Side Entrance",
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
|
||||
## Error Response Format
|
||||
|
||||
All endpoints return consistent error responses:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Descriptive error message",
|
||||
"code": "ERROR_CODE",
|
||||
"details": {
|
||||
"field": "Additional error details"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Common Error Codes
|
||||
- `INVALID_CREDENTIALS`: Login failed
|
||||
- `TOKEN_EXPIRED`: JWT token has expired
|
||||
- `DEVICE_NOT_AUTHORIZED`: Device not registered for user
|
||||
- `OUTSIDE_GEOFENCE`: Location outside allowed area
|
||||
- `QR_CODE_INACTIVE`: QR code is disabled
|
||||
- `DUPLICATE_EVENT`: Cannot perform same clock action twice
|
||||
- `SECURITY_VIOLATION`: Security check failed
|
||||
- `VALIDATION_ERROR`: Request data validation failed
|
||||
|
||||
### HTTP Status Codes
|
||||
- `200`: Success
|
||||
- `201`: Created successfully
|
||||
- `400`: Bad Request (validation errors)
|
||||
- `401`: Unauthorized (invalid/missing token)
|
||||
- `403`: Forbidden (insufficient permissions)
|
||||
- `404`: Not Found
|
||||
- `409`: Conflict (duplicate data)
|
||||
- `500`: Internal Server Error
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
API endpoints are rate-limited to prevent abuse:
|
||||
|
||||
- **Authentication endpoints**: 5 requests per minute per IP
|
||||
- **Clock endpoints**: 10 requests per minute per user
|
||||
- **Location updates**: 60 requests per minute per user
|
||||
- **General endpoints**: 100 requests per minute per user
|
||||
|
||||
Rate limit headers are included in responses:
|
||||
```
|
||||
X-RateLimit-Limit: 100
|
||||
X-RateLimit-Remaining: 95
|
||||
X-RateLimit-Reset: 1642234567
|
||||
```
|
||||
|
||||
## Webhook Events (Future)
|
||||
|
||||
The API supports webhook notifications for real-time events:
|
||||
|
||||
### Clock Events
|
||||
```json
|
||||
{
|
||||
"event": "worker.clock_in",
|
||||
"data": {
|
||||
"userId": 123,
|
||||
"timestamp": "2024-01-15T08:00:00.000Z",
|
||||
"location": {
|
||||
"latitude": -6.2088,
|
||||
"longitude": 106.8456
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Security Alerts
|
||||
```json
|
||||
{
|
||||
"event": "security.violation",
|
||||
"data": {
|
||||
"userId": 123,
|
||||
"alertType": "gps_spoofing_detected",
|
||||
"severity": "high",
|
||||
"timestamp": "2024-01-15T08:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## SDK Examples
|
||||
|
||||
### JavaScript/TypeScript
|
||||
```javascript
|
||||
class NilaiClockAPI {
|
||||
constructor(baseURL, token) {
|
||||
this.baseURL = baseURL
|
||||
this.token = token
|
||||
}
|
||||
|
||||
async clockIn(userId, qrCodeValue, latitude, longitude) {
|
||||
const response = await fetch(`${this.baseURL}/api/clock`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId,
|
||||
eventType: 'clock_in',
|
||||
qrCodeValue,
|
||||
latitude,
|
||||
longitude
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Clock in failed: ${response.status}`)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### cURL Examples
|
||||
```bash
|
||||
# Login
|
||||
curl -X POST https://myapp.ouji.com/nilai_clock_api/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"worker123","password":"password","deviceUuid":"device-uuid"}'
|
||||
|
||||
# Clock In
|
||||
curl -X POST https://myapp.ouji.com/nilai_clock_api/api/clock \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"userId":123,"eventType":"clock_in","qrCodeValue":"qr-uuid","latitude":-6.2088,"longitude":106.8456}'
|
||||
|
||||
# Get History
|
||||
curl -X GET https://myapp.ouji.com/nilai_clock_api/api/worker/clock-history/123 \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
@@ -0,0 +1,457 @@
|
||||
# Database Schema Documentation - Nilai Clock Client
|
||||
|
||||
## Overview
|
||||
|
||||
The Nilai Clock Client uses a MySQL 5.x database with MyISAM engine. The schema is designed for simplicity and performance, with consolidated device tracking and streamlined location updates.
|
||||
|
||||
## Database Configuration
|
||||
|
||||
### Connection Settings
|
||||
```javascript
|
||||
// Backend database configuration
|
||||
const dbConfig = {
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
port: process.env.DB_PORT,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
// MySQL 5.x specific settings
|
||||
charset: 'utf8',
|
||||
timezone: 'local'
|
||||
}
|
||||
```
|
||||
|
||||
### Engine and Charset
|
||||
- **Engine**: MyISAM (MySQL 5.x compatibility)
|
||||
- **Charset**: UTF-8
|
||||
- **Collation**: utf8_general_ci
|
||||
|
||||
## Core Tables
|
||||
|
||||
### workers
|
||||
Primary user accounts table for both workers and managers.
|
||||
|
||||
```sql
|
||||
CREATE TABLE `workers` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(255) NOT NULL,
|
||||
`password_hash` varchar(255) NOT NULL COMMENT 'Store hashed passwords, not plain text!',
|
||||
`full_name` varchar(255) NOT NULL,
|
||||
`role` enum('worker','manager') NOT NULL,
|
||||
`device_uuid` varchar(255) DEFAULT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `username` (`username`),
|
||||
KEY `idx_device_uuid` (`device_uuid`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Stores user account information for both workers and managers';
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- Consolidated device tracking via `device_uuid` field
|
||||
- Role-based access (worker/manager)
|
||||
- Unique username constraint
|
||||
- Indexed device UUID for fast lookups
|
||||
|
||||
**Sample Data:**
|
||||
```sql
|
||||
INSERT INTO workers (username, password_hash, full_name, role, device_uuid) VALUES
|
||||
('worker001', '$2b$10$hash...', 'John Doe', 'worker', 'android-uuid-123'),
|
||||
('manager001', '$2b$10$hash...', 'Jane Smith', 'manager', NULL);
|
||||
```
|
||||
|
||||
### clock_records
|
||||
Time tracking events with location data.
|
||||
|
||||
```sql
|
||||
CREATE TABLE `clock_records` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`worker_id` int(11) NOT NULL,
|
||||
`event_type` enum('clock_in','clock_out','failed') NOT NULL,
|
||||
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`qr_code_id` varchar(255) DEFAULT NULL,
|
||||
`latitude` decimal(10,8) DEFAULT NULL,
|
||||
`longitude` decimal(11,8) DEFAULT NULL,
|
||||
`notes` text,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_worker_id` (`worker_id`),
|
||||
KEY `idx_event_type` (`event_type`),
|
||||
KEY `idx_timestamp` (`timestamp`),
|
||||
KEY `idx_qr_code_id` (`qr_code_id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Records of worker clock in/out events';
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- Links to workers table via `worker_id`
|
||||
- Event types: clock_in, clock_out, failed
|
||||
- Optional QR code reference
|
||||
- Precise location coordinates (8 decimal places)
|
||||
- Optional notes field for manual entries
|
||||
|
||||
**Sample Data:**
|
||||
```sql
|
||||
INSERT INTO clock_records (worker_id, event_type, qr_code_id, latitude, longitude) VALUES
|
||||
(1, 'clock_in', 'qr-uuid-123', -6.20880000, 106.84560000),
|
||||
(1, 'clock_out', 'qr-uuid-123', -6.20880000, 106.84560000);
|
||||
```
|
||||
|
||||
### qr_codes
|
||||
QR code definitions and status.
|
||||
|
||||
```sql
|
||||
CREATE TABLE `qr_codes` (
|
||||
`id` varchar(255) NOT NULL,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`is_active` tinyint(1) NOT NULL DEFAULT '1',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_is_active` (`is_active`),
|
||||
KEY `idx_name` (`name`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='QR codes for clock in/out locations';
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- UUID-based primary key
|
||||
- Human-readable name
|
||||
- Active/inactive status
|
||||
- Creation timestamp
|
||||
|
||||
**Sample Data:**
|
||||
```sql
|
||||
INSERT INTO qr_codes (id, name, is_active) VALUES
|
||||
('550e8400-e29b-41d4-a716-446655440000', 'Main Entrance', 1),
|
||||
('550e8400-e29b-41d4-a716-446655440001', 'Side Gate', 1),
|
||||
('550e8400-e29b-41d4-a716-446655440002', 'Warehouse Door', 0);
|
||||
```
|
||||
|
||||
### location_updates
|
||||
Simplified location tracking for background monitoring.
|
||||
|
||||
```sql
|
||||
CREATE TABLE `location_updates` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`longitude` decimal(11,8) NOT NULL,
|
||||
`latitude` decimal(10,8) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Background location updates for tracking';
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- Simplified schema with only essential fields
|
||||
- Longitude before latitude (geographic convention)
|
||||
- User ID reference to workers table
|
||||
- Timestamp for tracking history
|
||||
|
||||
**Sample Data:**
|
||||
```sql
|
||||
INSERT INTO location_updates (user_id, longitude, latitude) VALUES
|
||||
(1, 106.84560000, -6.20880000),
|
||||
(1, 106.84561000, -6.20881000);
|
||||
```
|
||||
|
||||
## Security Tables
|
||||
|
||||
### security_alerts
|
||||
Security violation logging.
|
||||
|
||||
```sql
|
||||
CREATE TABLE `security_alerts` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`alert_type` varchar(100) NOT NULL,
|
||||
`alert_data` json DEFAULT NULL,
|
||||
`severity` enum('low','medium','high','critical') DEFAULT 'medium',
|
||||
`is_resolved` tinyint(1) DEFAULT '0',
|
||||
`resolved_at` timestamp NULL DEFAULT NULL,
|
||||
`resolved_by` int(11) DEFAULT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `resolved_by` (`resolved_by`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_alert_type` (`alert_type`),
|
||||
KEY `idx_severity` (`severity`),
|
||||
KEY `idx_is_resolved` (`is_resolved`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Security alerts and violations';
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- JSON data field for flexible alert information
|
||||
- Severity levels: low, medium, high, critical
|
||||
- Resolution tracking with timestamp and resolver
|
||||
- Multiple indexes for efficient querying
|
||||
|
||||
**Sample Data:**
|
||||
```sql
|
||||
INSERT INTO security_alerts (user_id, alert_type, alert_data, severity) VALUES
|
||||
(1, 'gps_spoofing_detected', '{"apps": ["com.fakegps"], "location": {"lat": -6.2088, "lng": 106.8456}}', 'high'),
|
||||
(1, 'location_anomaly', '{"distance": 500, "time": 60}', 'medium');
|
||||
```
|
||||
|
||||
### security_checks
|
||||
Periodic security assessments.
|
||||
|
||||
```sql
|
||||
CREATE TABLE `security_checks` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) NOT NULL,
|
||||
`timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`device_info` json DEFAULT NULL,
|
||||
`security_data` json DEFAULT NULL,
|
||||
`risk_level` enum('low','medium','high') DEFAULT 'low',
|
||||
`risk_score` int(11) DEFAULT '0',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_risk_level` (`risk_level`),
|
||||
KEY `idx_timestamp` (`timestamp`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Periodic security check results';
|
||||
```
|
||||
|
||||
**Key Features:**
|
||||
- JSON fields for flexible device and security data
|
||||
- Risk level and numeric score
|
||||
- Timestamp for tracking check frequency
|
||||
|
||||
**Sample Data:**
|
||||
```sql
|
||||
INSERT INTO security_checks (user_id, device_info, security_data, risk_level, risk_score) VALUES
|
||||
(1, '{"platform": "android", "model": "Pixel 6"}', '{"apps": [], "rooted": false}', 'low', 10);
|
||||
```
|
||||
|
||||
## Management Tables
|
||||
|
||||
### tags
|
||||
Worker categorization tags.
|
||||
|
||||
```sql
|
||||
CREATE TABLE `tags` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`tag_name` varchar(100) NOT NULL,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `tag_name` (`tag_name`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Tags for categorizing workers';
|
||||
```
|
||||
|
||||
### worker_tags
|
||||
Many-to-many relationship between workers and tags.
|
||||
|
||||
```sql
|
||||
CREATE TABLE `worker_tags` (
|
||||
`worker_id` int(11) NOT NULL,
|
||||
`tag_id` int(11) NOT NULL,
|
||||
PRIMARY KEY (`worker_id`,`tag_id`),
|
||||
KEY `idx_worker_id` (`worker_id`),
|
||||
KEY `idx_tag_id` (`tag_id`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Worker tag assignments';
|
||||
```
|
||||
|
||||
**Sample Data:**
|
||||
```sql
|
||||
INSERT INTO tags (tag_name) VALUES ('Production'), ('Day Shift'), ('Night Shift');
|
||||
INSERT INTO worker_tags (worker_id, tag_id) VALUES (1, 1), (1, 2);
|
||||
```
|
||||
|
||||
## Database Relationships
|
||||
|
||||
### Entity Relationship Diagram
|
||||
```
|
||||
workers (1) ----< (M) clock_records
|
||||
workers (1) ----< (M) location_updates
|
||||
workers (1) ----< (M) security_alerts
|
||||
workers (1) ----< (M) security_checks
|
||||
workers (M) ----< (M) worker_tags >---- (M) tags
|
||||
qr_codes (1) ----< (M) clock_records
|
||||
```
|
||||
|
||||
### Foreign Key Relationships (Logical)
|
||||
Note: MyISAM engine doesn't support foreign key constraints, but logical relationships exist:
|
||||
|
||||
- `clock_records.worker_id` → `workers.id`
|
||||
- `clock_records.qr_code_id` → `qr_codes.id`
|
||||
- `location_updates.user_id` → `workers.id`
|
||||
- `security_alerts.user_id` → `workers.id`
|
||||
- `security_checks.user_id` → `workers.id`
|
||||
- `worker_tags.worker_id` → `workers.id`
|
||||
- `worker_tags.tag_id` → `tags.id`
|
||||
|
||||
## Indexing Strategy
|
||||
|
||||
### Primary Indexes
|
||||
- All tables have auto-increment primary keys
|
||||
- Unique constraints on usernames and tag names
|
||||
|
||||
### Performance Indexes
|
||||
```sql
|
||||
-- Workers table
|
||||
KEY `idx_device_uuid` (`device_uuid`)
|
||||
|
||||
-- Clock records table
|
||||
KEY `idx_worker_id` (`worker_id`)
|
||||
KEY `idx_event_type` (`event_type`)
|
||||
KEY `idx_timestamp` (`timestamp`)
|
||||
KEY `idx_qr_code_id` (`qr_code_id`)
|
||||
|
||||
-- Location updates table
|
||||
KEY `idx_user_id` (`user_id`)
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
|
||||
-- Security tables
|
||||
KEY `idx_user_id` (`user_id`)
|
||||
KEY `idx_alert_type` (`alert_type`)
|
||||
KEY `idx_severity` (`severity`)
|
||||
KEY `idx_risk_level` (`risk_level`)
|
||||
```
|
||||
|
||||
## Common Queries
|
||||
|
||||
### Worker Authentication
|
||||
```sql
|
||||
-- Login verification
|
||||
SELECT id, role, password_hash
|
||||
FROM workers
|
||||
WHERE username = ?;
|
||||
|
||||
-- Device validation
|
||||
SELECT id, device_uuid
|
||||
FROM workers
|
||||
WHERE id = ? AND device_uuid = ?;
|
||||
```
|
||||
|
||||
### Clock Operations
|
||||
```sql
|
||||
-- Record clock event
|
||||
INSERT INTO clock_records (worker_id, event_type, qr_code_id, latitude, longitude)
|
||||
VALUES (?, ?, ?, ?, ?);
|
||||
|
||||
-- Get worker history
|
||||
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;
|
||||
```
|
||||
|
||||
### Location Tracking
|
||||
```sql
|
||||
-- Insert location update
|
||||
INSERT INTO location_updates (user_id, longitude, latitude)
|
||||
VALUES (?, ?, ?);
|
||||
|
||||
-- Get recent locations
|
||||
SELECT longitude, latitude, created_at
|
||||
FROM location_updates
|
||||
WHERE user_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL 1 DAY)
|
||||
ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
### Security Monitoring
|
||||
```sql
|
||||
-- Log security alert
|
||||
INSERT INTO security_alerts (user_id, alert_type, alert_data, severity)
|
||||
VALUES (?, ?, ?, ?);
|
||||
|
||||
-- Get security status
|
||||
SELECT * FROM security_checks
|
||||
WHERE user_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1;
|
||||
```
|
||||
|
||||
## Data Retention Policies
|
||||
|
||||
### Location Data
|
||||
- Keep location_updates for 30 days
|
||||
- Archive older data to separate table
|
||||
- Clean up automatically via scheduled job
|
||||
|
||||
### Security Data
|
||||
- Keep security_alerts indefinitely for audit
|
||||
- Keep security_checks for 90 days
|
||||
- Compress old data for storage efficiency
|
||||
|
||||
### Clock Records
|
||||
- Keep all clock_records permanently
|
||||
- Essential for payroll and compliance
|
||||
- Regular backup and archival
|
||||
|
||||
## Backup Strategy
|
||||
|
||||
### Daily Backups
|
||||
```bash
|
||||
# Full database backup
|
||||
mysqldump -u username -p database_name > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Table-specific backups
|
||||
mysqldump -u username -p database_name workers clock_records > critical_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### Incremental Backups
|
||||
```bash
|
||||
# Binary log backup for point-in-time recovery
|
||||
mysqlbinlog --start-datetime="2024-01-01 00:00:00" mysql-bin.000001 > incremental.sql
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Query Optimization
|
||||
- Use EXPLAIN to analyze query performance
|
||||
- Add indexes for frequently queried columns
|
||||
- Limit result sets with appropriate WHERE clauses
|
||||
|
||||
### Table Optimization
|
||||
```sql
|
||||
-- Optimize tables periodically
|
||||
OPTIMIZE TABLE workers, clock_records, location_updates;
|
||||
|
||||
-- Analyze tables for better query planning
|
||||
ANALYZE TABLE workers, clock_records, location_updates;
|
||||
```
|
||||
|
||||
### Partitioning (Future Enhancement)
|
||||
```sql
|
||||
-- Partition location_updates by date for better performance
|
||||
ALTER TABLE location_updates
|
||||
PARTITION BY RANGE (TO_DAYS(created_at)) (
|
||||
PARTITION p_2024_01 VALUES LESS THAN (TO_DAYS('2024-02-01')),
|
||||
PARTITION p_2024_02 VALUES LESS THAN (TO_DAYS('2024-03-01')),
|
||||
-- Add more partitions as needed
|
||||
);
|
||||
```
|
||||
|
||||
## Migration Scripts
|
||||
|
||||
### Version Updates
|
||||
```sql
|
||||
-- Example migration script for adding new columns
|
||||
ALTER TABLE workers ADD COLUMN last_login timestamp NULL DEFAULT NULL;
|
||||
ALTER TABLE workers ADD KEY idx_last_login (last_login);
|
||||
|
||||
-- Update existing data
|
||||
UPDATE workers SET last_login = created_at WHERE last_login IS NULL;
|
||||
```
|
||||
|
||||
### Data Cleanup
|
||||
```sql
|
||||
-- Clean old location data (older than 30 days)
|
||||
DELETE FROM location_updates
|
||||
WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);
|
||||
|
||||
-- Archive old security checks
|
||||
INSERT INTO security_checks_archive
|
||||
SELECT * FROM security_checks
|
||||
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY);
|
||||
|
||||
DELETE FROM security_checks
|
||||
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY);
|
||||
```
|
||||
@@ -0,0 +1,878 @@
|
||||
# iOS Migration Guide - Nilai Clock Client
|
||||
|
||||
## Overview
|
||||
|
||||
This guide provides a comprehensive step-by-step process for migrating the Nilai Clock Client from Android-only to support iOS. The migration involves platform-specific implementations for background location tracking, security features, and native iOS integrations.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Development Environment
|
||||
- **macOS**: Required for iOS development
|
||||
- **Xcode 14+**: Latest version recommended
|
||||
- **iOS Simulator**: For testing
|
||||
- **Apple Developer Account**: For device testing and App Store deployment
|
||||
- **Node.js 18+**: Current project requirement
|
||||
- **Capacitor CLI**: Already installed in project
|
||||
|
||||
### Project Knowledge
|
||||
- Understanding of current Android implementation
|
||||
- Familiarity with Capacitor plugin architecture
|
||||
- Knowledge of iOS development basics
|
||||
- Understanding of iOS permissions and background modes
|
||||
|
||||
## Phase 1: Environment Setup
|
||||
|
||||
### Step 1.1: Install iOS Platform
|
||||
```bash
|
||||
# Navigate to project directory
|
||||
cd Nilai_Clock_Client
|
||||
|
||||
# Add iOS platform to Capacitor
|
||||
npm install @capacitor/ios
|
||||
npx cap add ios
|
||||
|
||||
# Verify iOS platform added
|
||||
ls -la ios/
|
||||
```
|
||||
|
||||
### Step 1.2: Initial iOS Configuration
|
||||
```bash
|
||||
# Copy web assets to iOS
|
||||
npx cap copy ios
|
||||
|
||||
# Sync Capacitor plugins with iOS
|
||||
npx cap sync ios
|
||||
|
||||
# Open project in Xcode
|
||||
npx cap open ios
|
||||
```
|
||||
|
||||
### Step 1.3: Configure Xcode Project
|
||||
1. **Open in Xcode**: Project should open automatically
|
||||
2. **Set Team**: Select your Apple Developer team
|
||||
3. **Bundle Identifier**: Verify `com.ouji.factory.myapp`
|
||||
4. **Deployment Target**: Set to iOS 13.0 minimum
|
||||
5. **Signing**: Configure automatic signing
|
||||
|
||||
### Step 1.4: Update Capacitor Configuration
|
||||
```json
|
||||
// capacitor.config.json - Add iOS section
|
||||
{
|
||||
"appId": "com.ouji.factory.myapp",
|
||||
"appName": "nilai-clock",
|
||||
"webDir": "dist",
|
||||
"android": {
|
||||
"useLegacyBridge": true
|
||||
},
|
||||
"ios": {
|
||||
"scheme": "NilaiClock",
|
||||
"contentInset": "automatic",
|
||||
"backgroundColor": "#2563eb"
|
||||
},
|
||||
"plugins": {
|
||||
"SafeArea": {
|
||||
"enabled": true,
|
||||
"customColorsForSystemBars": true,
|
||||
"statusBarColor": "#2563eb",
|
||||
"statusBarContent": "light",
|
||||
"navigationBarColor": "#ffffff",
|
||||
"navigationBarContent": "dark",
|
||||
"offset": 0
|
||||
}
|
||||
// ... other plugins
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 2: iOS Permissions and Info.plist
|
||||
|
||||
### Step 2.1: Configure Info.plist
|
||||
Create or update `ios/App/App/Info.plist`:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- App Information -->
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Nilai Clock</string>
|
||||
|
||||
<!-- Location Permissions -->
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>This app needs location access to track work attendance and ensure workers are at authorized locations during work hours.</string>
|
||||
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>This app needs location access to verify your work location when clocking in/out.</string>
|
||||
|
||||
<!-- Camera Permission for QR Scanning -->
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes for clocking in/out.</string>
|
||||
|
||||
<!-- Background Modes -->
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>location</string>
|
||||
<string>background-processing</string>
|
||||
<string>background-fetch</string>
|
||||
</array>
|
||||
|
||||
<!-- App Transport Security -->
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
<!-- For production, use specific domain exceptions -->
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>myapp.ouji.com</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<false/>
|
||||
<key>NSExceptionMinimumTLSVersion</key>
|
||||
<string>TLSv1.2</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
||||
<!-- Prevent App Backup of Sensitive Data -->
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
|
||||
<!-- Status Bar Configuration -->
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleLightContent</string>
|
||||
|
||||
<!-- Launch Screen -->
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
|
||||
<!-- Supported Interface Orientations -->
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
|
||||
<!-- Requires Full Screen -->
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### Step 2.2: Configure Entitlements
|
||||
Create `ios/App/App/App.entitlements`:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Background Modes -->
|
||||
<key>com.apple.developer.location.background</key>
|
||||
<true/>
|
||||
|
||||
<!-- Keychain Sharing -->
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.ouji.factory.myapp</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
## Phase 3: iOS-Specific Plugin Implementation
|
||||
|
||||
### Step 3.1: Create iOS AppSecurity Plugin
|
||||
|
||||
Create directory structure:
|
||||
```bash
|
||||
mkdir -p ios/App/App/Plugins
|
||||
```
|
||||
|
||||
Create `ios/App/App/Plugins/AppSecurity.swift`:
|
||||
|
||||
```swift
|
||||
import Foundation
|
||||
import Capacitor
|
||||
import UIKit
|
||||
|
||||
@objc(AppSecurity)
|
||||
public class AppSecurity: CAPPlugin {
|
||||
|
||||
@objc func getInstalledApps(_ call: CAPPluginCall) {
|
||||
// iOS doesn't allow listing installed apps for privacy
|
||||
// Alternative: Check for specific URL schemes of known spoofing apps
|
||||
let suspiciousSchemes = [
|
||||
"fakegps://",
|
||||
"locationspoof://",
|
||||
"gpscheat://",
|
||||
"mockgps://"
|
||||
]
|
||||
|
||||
var detectedSchemes: [String] = []
|
||||
|
||||
for scheme in suspiciousSchemes {
|
||||
if let url = URL(string: scheme) {
|
||||
if UIApplication.shared.canOpenURL(url) {
|
||||
detectedSchemes.append(scheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result: [String: Any] = [
|
||||
"packages": [], // iOS doesn't provide package names
|
||||
"detectedSchemes": detectedSchemes,
|
||||
"platform": "ios"
|
||||
]
|
||||
|
||||
call.resolve(result)
|
||||
}
|
||||
|
||||
@objc func checkJailbreakStatus(_ call: CAPPluginCall) {
|
||||
let isJailbroken = detectJailbreak()
|
||||
let result: [String: Any] = [
|
||||
"isJailbroken": isJailbroken,
|
||||
"riskLevel": isJailbroken ? "high" : "low"
|
||||
]
|
||||
call.resolve(result)
|
||||
}
|
||||
|
||||
@objc func performSecurityCheck(_ call: CAPPluginCall) {
|
||||
let jailbroken = detectJailbreak()
|
||||
let debuggerAttached = isDebuggerAttached()
|
||||
let simulator = isSimulator()
|
||||
|
||||
var riskLevel = "low"
|
||||
var riskFactors: [String] = []
|
||||
|
||||
if jailbroken {
|
||||
riskLevel = "high"
|
||||
riskFactors.append("jailbreak_detected")
|
||||
}
|
||||
|
||||
if debuggerAttached {
|
||||
riskLevel = "medium"
|
||||
riskFactors.append("debugger_attached")
|
||||
}
|
||||
|
||||
if simulator {
|
||||
riskFactors.append("simulator_detected")
|
||||
}
|
||||
|
||||
let result: [String: Any] = [
|
||||
"riskLevel": riskLevel,
|
||||
"riskFactors": riskFactors,
|
||||
"deviceInfo": getDeviceInfo(),
|
||||
"timestamp": ISO8601DateFormatter().string(from: Date())
|
||||
]
|
||||
|
||||
call.resolve(result)
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private func detectJailbreak() -> Bool {
|
||||
// Check for jailbreak indicators
|
||||
let jailbreakPaths = [
|
||||
"/Applications/Cydia.app",
|
||||
"/Library/MobileSubstrate/MobileSubstrate.dylib",
|
||||
"/bin/bash",
|
||||
"/usr/sbin/sshd",
|
||||
"/etc/apt",
|
||||
"/private/var/lib/apt/",
|
||||
"/private/var/lib/cydia",
|
||||
"/private/var/mobile/Library/SBSettings/Themes",
|
||||
"/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
|
||||
"/System/Library/LaunchDaemons/com.ikey.bbot.plist",
|
||||
"/private/var/cache/apt",
|
||||
"/private/var/lib/apt",
|
||||
"/private/var/lib/cydia",
|
||||
"/private/var/log/syslog",
|
||||
"/private/var/tmp/cydia.log",
|
||||
"/Applications/Icy.app",
|
||||
"/Applications/MxTube.app",
|
||||
"/Applications/RockApp.app",
|
||||
"/Applications/blackra1n.app",
|
||||
"/Applications/SBSettings.app",
|
||||
"/Applications/FakeCarrier.app",
|
||||
"/Applications/WinterBoard.app",
|
||||
"/Applications/IntelliScreen.app"
|
||||
]
|
||||
|
||||
for path in jailbreakPaths {
|
||||
if FileManager.default.fileExists(atPath: path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we can write to system directories
|
||||
let testPath = "/private/test_jailbreak.txt"
|
||||
do {
|
||||
try "test".write(toFile: testPath, atomically: true, encoding: .utf8)
|
||||
try FileManager.default.removeItem(atPath: testPath)
|
||||
return true // Should not be able to write here
|
||||
} catch {
|
||||
// Good, we can't write to system directories
|
||||
}
|
||||
|
||||
// Check for suspicious URL schemes
|
||||
let suspiciousSchemes = ["cydia://", "sileo://", "zbra://"]
|
||||
for scheme in suspiciousSchemes {
|
||||
if let url = URL(string: scheme) {
|
||||
if UIApplication.shared.canOpenURL(url) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func isDebuggerAttached() -> Bool {
|
||||
var info = kinfo_proc()
|
||||
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
|
||||
var size = MemoryLayout<kinfo_proc>.stride
|
||||
|
||||
let result = sysctl(&mib, u_int(mib.count), &info, &size, nil, 0)
|
||||
|
||||
if result != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return (info.kp_proc.p_flag & P_TRACED) != 0
|
||||
}
|
||||
|
||||
private func isSimulator() -> Bool {
|
||||
#if targetEnvironment(simulator)
|
||||
return true
|
||||
#else
|
||||
return false
|
||||
#endif
|
||||
}
|
||||
|
||||
private func getDeviceInfo() -> [String: Any] {
|
||||
let device = UIDevice.current
|
||||
return [
|
||||
"platform": "ios",
|
||||
"model": device.model,
|
||||
"systemName": device.systemName,
|
||||
"systemVersion": device.systemVersion,
|
||||
"name": device.name,
|
||||
"isSimulator": isSimulator()
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3.2: Register Plugin in iOS
|
||||
|
||||
Update `ios/App/App/AppDelegate.swift`:
|
||||
|
||||
```swift
|
||||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state.
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive.
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate.
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Called when the app was launched with a url.
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
// Called when the app was launched with an activity, including Universal Links.
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Create `ios/App/App/Plugins/AppSecurity.m` (Objective-C bridge):
|
||||
|
||||
```objc
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Capacitor/Capacitor.h>
|
||||
|
||||
CAP_PLUGIN(AppSecurity, "AppSecurity",
|
||||
CAP_PLUGIN_METHOD(getInstalledApps, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(checkJailbreakStatus, CAPPluginReturnPromise);
|
||||
CAP_PLUGIN_METHOD(performSecurityCheck, CAPPluginReturnPromise);
|
||||
)
|
||||
```
|
||||
|
||||
## Phase 4: Platform-Specific Code Adaptations
|
||||
|
||||
### Step 4.1: Update Device UUID Service
|
||||
|
||||
Modify `src/services/deviceUuidService.js` to handle iOS:
|
||||
|
||||
```javascript
|
||||
// Add iOS-specific UUID generation
|
||||
async generateDeviceUuid() {
|
||||
try {
|
||||
let baseString = ''
|
||||
|
||||
if (this.isNative) {
|
||||
const deviceInfo = await Device.getInfo()
|
||||
const deviceId = await Device.getId()
|
||||
|
||||
if (deviceInfo.platform === 'ios') {
|
||||
// iOS-specific UUID generation
|
||||
baseString = await this.generateIOSDeviceString(deviceInfo, deviceId)
|
||||
} else {
|
||||
// Android implementation (existing)
|
||||
baseString = [
|
||||
deviceInfo.platform || 'unknown',
|
||||
deviceInfo.model || 'unknown',
|
||||
deviceInfo.manufacturer || 'unknown',
|
||||
deviceId.identifier || 'unknown',
|
||||
deviceInfo.osVersion || 'unknown'
|
||||
].join('-')
|
||||
}
|
||||
} else {
|
||||
// Web fallback
|
||||
baseString = [
|
||||
navigator.userAgent,
|
||||
navigator.language,
|
||||
screen.width,
|
||||
screen.height,
|
||||
new Date().getTimezoneOffset()
|
||||
].join('-')
|
||||
}
|
||||
|
||||
// Generate UUID from base string using crypto
|
||||
return await this.hashString(baseString)
|
||||
} catch (error) {
|
||||
console.error('Failed to generate device UUID:', error)
|
||||
return this.generateFallbackUuid()
|
||||
}
|
||||
}
|
||||
|
||||
async generateIOSDeviceString(deviceInfo, deviceId) {
|
||||
// iOS provides limited device identification
|
||||
// Use a combination of available identifiers and stored values
|
||||
|
||||
try {
|
||||
// Check for existing iOS-specific identifier
|
||||
const { value: existingId } = await Preferences.get({ key: 'ios_device_persistent_id' })
|
||||
|
||||
if (existingId) {
|
||||
return [
|
||||
'ios',
|
||||
deviceInfo.osVersion || 'unknown',
|
||||
existingId,
|
||||
deviceId.identifier || 'unknown'
|
||||
].join('-')
|
||||
}
|
||||
|
||||
// Generate new persistent identifier for iOS
|
||||
const persistentId = this.generateRandomId(32)
|
||||
await Preferences.set({
|
||||
key: 'ios_device_persistent_id',
|
||||
value: persistentId
|
||||
})
|
||||
|
||||
return [
|
||||
'ios',
|
||||
deviceInfo.osVersion || 'unknown',
|
||||
persistentId,
|
||||
deviceId.identifier || 'unknown'
|
||||
].join('-')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to generate iOS device string:', error)
|
||||
return `ios-fallback-${Date.now()}`
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4.2: Update Background Location Service
|
||||
|
||||
Modify `src/services/backgroundLocationService.js` for iOS compatibility:
|
||||
|
||||
```javascript
|
||||
// Add iOS-specific initialization
|
||||
async initialize() {
|
||||
if (!this.isNative) {
|
||||
console.warn('Background location is only available on native platforms')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const deviceInfo = await Device.getInfo()
|
||||
|
||||
if (deviceInfo.platform === 'ios') {
|
||||
await this.initializeIOS()
|
||||
} else {
|
||||
await this.initializeAndroid()
|
||||
}
|
||||
|
||||
this.isInitialized = true
|
||||
console.log(`Background location initialized for ${deviceInfo.platform}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize background location:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async initializeIOS() {
|
||||
console.log('Initializing iOS background location...')
|
||||
|
||||
// Request location permissions first
|
||||
const permissions = await Geolocation.requestPermissions()
|
||||
console.log('iOS location permissions:', permissions)
|
||||
|
||||
if (permissions.location !== 'granted') {
|
||||
throw new Error('Location permission not granted')
|
||||
}
|
||||
|
||||
// Initialize background geolocation with iOS-specific config
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
const messages = this.getNotificationMessages()
|
||||
|
||||
try {
|
||||
this.watcherId = await BackgroundGeolocation.addWatcher(
|
||||
{
|
||||
requestPermissions: true,
|
||||
stale: false,
|
||||
distanceFilter: 50,
|
||||
backgroundMessage: messages.message,
|
||||
backgroundTitle: messages.title,
|
||||
// iOS-specific options
|
||||
pausesLocationUpdatesAutomatically: false,
|
||||
allowsBackgroundLocationUpdates: true,
|
||||
showsBackgroundLocationIndicator: false, // Set to true for debugging
|
||||
desiredAccuracy: 'high'
|
||||
},
|
||||
(location, error) => {
|
||||
if (error) {
|
||||
console.error('iOS background location error:', error)
|
||||
return
|
||||
}
|
||||
if (location) {
|
||||
console.log('iOS background location update:', location)
|
||||
this.handleLocationUpdate(location)
|
||||
}
|
||||
}
|
||||
)
|
||||
console.log(`iOS BackgroundGeolocation watcher added with ID: ${this.watcherId}`)
|
||||
} catch (watcherError) {
|
||||
console.error('Failed to add iOS BackgroundGeolocation watcher:', watcherError)
|
||||
this.watcherId = null
|
||||
throw watcherError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iOS-specific location permission handling
|
||||
async requestIOSLocationPermissions() {
|
||||
try {
|
||||
// First request "when in use" permission
|
||||
let permissions = await Geolocation.requestPermissions()
|
||||
|
||||
if (permissions.location === 'granted') {
|
||||
// Try to upgrade to "always" permission
|
||||
// Note: iOS requires user to manually change this in Settings
|
||||
console.log('Location permission granted. User may need to change to "Always" in Settings.')
|
||||
return true
|
||||
}
|
||||
|
||||
throw new Error(`Location permission denied: ${permissions.location}`)
|
||||
} catch (error) {
|
||||
console.error('Failed to request iOS location permissions:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4.3: Update Anti-Spoofing Service
|
||||
|
||||
Modify `src/services/antiSpoofingService.js` for iOS:
|
||||
|
||||
```javascript
|
||||
// Add iOS-specific security checks
|
||||
async performSecurityCheck() {
|
||||
if (!this.isNative) {
|
||||
console.warn('Security checks only available on native platforms')
|
||||
return { riskLevel: 'unknown', platform: 'web' }
|
||||
}
|
||||
|
||||
try {
|
||||
const deviceInfo = await Device.getInfo()
|
||||
|
||||
if (deviceInfo.platform === 'ios') {
|
||||
return await this.performIOSSecurityCheck()
|
||||
} else {
|
||||
return await this.performAndroidSecurityCheck()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Security check failed:', error)
|
||||
return { riskLevel: 'unknown', error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
async performIOSSecurityCheck() {
|
||||
try {
|
||||
// Use custom iOS AppSecurity plugin
|
||||
const securityResult = await AppSecurity.performSecurityCheck()
|
||||
|
||||
// Send results to server
|
||||
await this.sendSecurityCheckToServer({
|
||||
platform: 'ios',
|
||||
...securityResult
|
||||
})
|
||||
|
||||
// Handle high-risk scenarios
|
||||
if (securityResult.riskLevel === 'high') {
|
||||
await this.handleSecurityViolation('ios_security_risk', securityResult)
|
||||
}
|
||||
|
||||
return securityResult
|
||||
} catch (error) {
|
||||
console.error('iOS security check failed:', error)
|
||||
return { riskLevel: 'unknown', error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
async handleIOSJailbreakDetection(isJailbroken) {
|
||||
if (isJailbroken) {
|
||||
console.warn('iOS jailbreak detected!')
|
||||
|
||||
// Log security alert
|
||||
await this.sendSecurityAlert({
|
||||
alertType: 'ios_jailbreak_detected',
|
||||
severity: 'high',
|
||||
data: {
|
||||
platform: 'ios',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
|
||||
// Optionally block app functionality
|
||||
this.dispatchSecurityEvent('app-blocked', {
|
||||
message: 'Security violation detected. Please contact your administrator.'
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 5: Testing and Validation
|
||||
|
||||
### Step 5.1: iOS Simulator Testing
|
||||
|
||||
```bash
|
||||
# Build and run on iOS simulator
|
||||
npm run build
|
||||
npx cap copy ios
|
||||
npx cap sync ios
|
||||
npx cap open ios
|
||||
|
||||
# In Xcode:
|
||||
# 1. Select iOS Simulator
|
||||
# 2. Choose device (iPhone 14, iPad, etc.)
|
||||
# 3. Click Run button
|
||||
```
|
||||
|
||||
### Step 5.2: iOS Device Testing
|
||||
|
||||
1. **Connect iOS Device**: Use USB cable
|
||||
2. **Trust Computer**: On device when prompted
|
||||
3. **Select Device**: In Xcode device list
|
||||
4. **Run on Device**: Click Run button in Xcode
|
||||
|
||||
### Step 5.3: iOS Testing Checklist
|
||||
|
||||
#### Location Services
|
||||
- [ ] Location permission request works
|
||||
- [ ] "When in Use" permission granted
|
||||
- [ ] Background location tracking starts
|
||||
- [ ] Location updates received in background
|
||||
- [ ] Geofence validation works
|
||||
- [ ] Location accuracy acceptable
|
||||
|
||||
#### Camera/QR Scanning
|
||||
- [ ] Camera permission request works
|
||||
- [ ] QR scanner opens camera
|
||||
- [ ] QR codes scan successfully
|
||||
- [ ] Camera closes after scan
|
||||
- [ ] Error handling for permission denial
|
||||
|
||||
#### Security Features
|
||||
- [ ] Jailbreak detection works
|
||||
- [ ] Security checks complete
|
||||
- [ ] Risk assessment accurate
|
||||
- [ ] Security alerts sent to server
|
||||
|
||||
#### App Lifecycle
|
||||
- [ ] App launches successfully
|
||||
- [ ] Background/foreground transitions
|
||||
- [ ] App state preservation
|
||||
- [ ] Memory management
|
||||
|
||||
#### Data Persistence
|
||||
- [ ] Secure storage works (Keychain)
|
||||
- [ ] Data survives app restart
|
||||
- [ ] Authentication tokens persist
|
||||
- [ ] User preferences saved
|
||||
|
||||
#### Network Connectivity
|
||||
- [ ] API calls work over WiFi
|
||||
- [ ] API calls work over cellular
|
||||
- [ ] Offline mode handling
|
||||
- [ ] Network error recovery
|
||||
|
||||
### Step 5.4: iOS-Specific Issues and Solutions
|
||||
|
||||
#### Common iOS Issues
|
||||
|
||||
**Issue: Background location stops working**
|
||||
```swift
|
||||
// Solution: Ensure proper background modes in Info.plist
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>location</string>
|
||||
<string>background-processing</string>
|
||||
</array>
|
||||
```
|
||||
|
||||
**Issue: Camera permission denied**
|
||||
```javascript
|
||||
// Solution: Check permission status and guide user
|
||||
const checkCameraPermission = async () => {
|
||||
const permissions = await Camera.requestPermissions()
|
||||
if (permissions.camera === 'denied') {
|
||||
// Guide user to Settings app
|
||||
alert('Please enable camera access in Settings > Privacy > Camera')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Issue: App crashes on device but works in simulator**
|
||||
```bash
|
||||
# Solution: Check device logs in Xcode
|
||||
# Window > Devices and Simulators > Select Device > View Device Logs
|
||||
```
|
||||
|
||||
**Issue: Location permission downgraded from "Always" to "When in Use"**
|
||||
```javascript
|
||||
// Solution: Monitor permission changes and re-request
|
||||
const monitorLocationPermissions = async () => {
|
||||
const permissions = await Geolocation.checkPermissions()
|
||||
if (permissions.location === 'granted-when-in-use') {
|
||||
// Inform user to change to "Always" in Settings
|
||||
showLocationPermissionAlert()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Phase 6: iOS Deployment
|
||||
|
||||
### Step 6.1: App Store Preparation
|
||||
|
||||
1. **Apple Developer Account**: Ensure active membership
|
||||
2. **App Store Connect**: Create app record
|
||||
3. **Certificates**: Generate distribution certificate
|
||||
4. **Provisioning Profile**: Create App Store profile
|
||||
5. **App Icons**: Prepare all required sizes
|
||||
6. **Screenshots**: Capture for all device sizes
|
||||
|
||||
### Step 6.2: Build for Distribution
|
||||
|
||||
```bash
|
||||
# Production build
|
||||
npm run build:production
|
||||
npx cap copy ios
|
||||
npx cap sync ios
|
||||
|
||||
# Open Xcode for archiving
|
||||
npx cap open ios
|
||||
```
|
||||
|
||||
In Xcode:
|
||||
1. **Select "Any iOS Device"** as target
|
||||
2. **Product > Archive** to create archive
|
||||
3. **Distribute App** when archive completes
|
||||
4. **Upload to App Store Connect**
|
||||
|
||||
### Step 6.3: App Store Review Preparation
|
||||
|
||||
#### Privacy Policy Requirements
|
||||
- Location usage justification
|
||||
- Camera usage explanation
|
||||
- Data collection practices
|
||||
- Third-party service usage
|
||||
|
||||
#### App Review Information
|
||||
```
|
||||
Location Usage: This app tracks worker location during work hours to verify attendance at authorized work sites. Location data is used only for work verification and is not shared with third parties.
|
||||
|
||||
Camera Usage: The app uses the camera to scan QR codes for clocking in/out at work locations. No photos are stored or transmitted.
|
||||
|
||||
Background Location: Required to ensure workers remain at authorized locations during work hours and for accurate time tracking.
|
||||
```
|
||||
|
||||
### Step 6.4: iOS Deployment Checklist
|
||||
|
||||
#### Pre-Submission
|
||||
- [ ] All iOS testing completed
|
||||
- [ ] App icons and metadata ready
|
||||
- [ ] Privacy policy updated
|
||||
- [ ] App Store screenshots captured
|
||||
- [ ] Age rating determined
|
||||
- [ ] Keywords and description optimized
|
||||
|
||||
#### Technical Requirements
|
||||
- [ ] iOS 13.0+ compatibility verified
|
||||
- [ ] All device sizes tested
|
||||
- [ ] Performance optimization completed
|
||||
- [ ] Memory usage optimized
|
||||
- [ ] Battery usage minimized
|
||||
|
||||
#### App Store Guidelines
|
||||
- [ ] Location usage clearly justified
|
||||
- [ ] No unnecessary permissions requested
|
||||
- [ ] User data protection implemented
|
||||
- [ ] Accessibility features supported
|
||||
- [ ] Content guidelines followed
|
||||
|
||||
## Conclusion
|
||||
|
||||
The iOS migration requires careful attention to platform-specific implementations, particularly for background location services and security features. The modular architecture of the current Android implementation makes the migration feasible, with most business logic remaining unchanged while native services require iOS-specific implementations.
|
||||
|
||||
Key success factors:
|
||||
1. **Proper iOS permissions and background modes configuration**
|
||||
2. **Platform-specific security implementations**
|
||||
3. **Thorough testing on iOS devices and simulators**
|
||||
4. **App Store compliance and review preparation**
|
||||
5. **User experience optimization for iOS patterns**
|
||||
|
||||
The migration timeline should allow for 4-6 weeks of development and testing, followed by 1-2 weeks for App Store review and approval.
|
||||
File diff suppressed because it is too large
Load Diff
+261
@@ -0,0 +1,261 @@
|
||||
# Nilai Clock Client - Android Time Tracking Application
|
||||
|
||||
## 📋 Project Overview
|
||||
|
||||
The Nilai Clock Client is a native Android application for worker time tracking, converted from a Vue.js web application using Capacitor. It provides QR code-based clock in/out functionality with background location tracking and comprehensive security features.
|
||||
|
||||
### Current Status
|
||||
- ✅ **Android**: Fully implemented with native features
|
||||
- ❌ **iOS**: Not implemented (migration guide available)
|
||||
- ⚠️ **Web**: Limited functionality (native features disabled)
|
||||
|
||||
## 📚 Complete Documentation
|
||||
|
||||
This project includes comprehensive documentation for development, deployment, and iOS migration:
|
||||
|
||||
- **[PROJECT_DOCUMENTATION.md](./PROJECT_DOCUMENTATION.md)** - Complete project overview and architecture
|
||||
- **[API_REFERENCE.md](./API_REFERENCE.md)** - Comprehensive API documentation
|
||||
- **[IOS_MIGRATION_GUIDE.md](./IOS_MIGRATION_GUIDE.md)** - Step-by-step iOS migration guide
|
||||
- **[ANDROID_IMPLEMENTATION_DETAILS.md](./ANDROID_IMPLEMENTATION_DETAILS.md)** - Android-specific details
|
||||
- **[DATABASE_SCHEMA.md](./DATABASE_SCHEMA.md)** - Database schema documentation
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- **Node.js**: 18.x or higher (tested with 22.14.0)
|
||||
- **Android Studio**: Latest version
|
||||
- **MySQL**: 5.x database server
|
||||
- **Capacitor CLI**: Included in dependencies
|
||||
|
||||
### Project Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Set up environment configuration
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
Create environment files for different deployment targets:
|
||||
|
||||
#### .env.local-http (Local Development)
|
||||
```env
|
||||
DB_HOST=localhost
|
||||
DB_USER=your_database_user
|
||||
DB_PASSWORD=your_database_password
|
||||
DB_NAME=your_database_name
|
||||
DB_PORT=3306
|
||||
VITE_API_BASE_URL=http://192.168.36.54:3000
|
||||
```
|
||||
|
||||
#### .env.production-server (Production)
|
||||
```env
|
||||
DB_HOST=your_production_host
|
||||
DB_USER=your_production_user
|
||||
DB_PASSWORD=your_production_password
|
||||
DB_NAME=your_production_database
|
||||
DB_PORT=3306
|
||||
VITE_API_BASE_URL=https://myapp.ouji.com/nilai_clock_api
|
||||
```
|
||||
|
||||
### Development Commands
|
||||
|
||||
#### Frontend Development
|
||||
```bash
|
||||
# Run frontend only
|
||||
npm run dev
|
||||
|
||||
# Run with HTTPS (for testing native features)
|
||||
npm run dev:https
|
||||
```
|
||||
|
||||
#### Backend Development
|
||||
```bash
|
||||
# Run backend only
|
||||
npm run backend
|
||||
|
||||
# Run backend with HTTPS
|
||||
npm run backend:https
|
||||
```
|
||||
|
||||
#### Full Stack Development
|
||||
```bash
|
||||
# Run both frontend and backend
|
||||
npm run dev:all
|
||||
|
||||
# Run both with HTTPS
|
||||
npm run dev:all:https
|
||||
```
|
||||
|
||||
### Android Development
|
||||
|
||||
#### Build for Android
|
||||
```bash
|
||||
# Switch to desired environment
|
||||
npm run switch:local-http # Local HTTP
|
||||
npm run switch:local-https # Local HTTPS
|
||||
npm run switch:production # Production
|
||||
|
||||
# Build and sync to Android
|
||||
npm run build:android
|
||||
|
||||
# Open in Android Studio
|
||||
npm run open:android
|
||||
```
|
||||
|
||||
#### Quick Build Commands
|
||||
```bash
|
||||
# Build for local HTTP testing
|
||||
npm run build:local-http
|
||||
|
||||
# Build for local HTTPS testing
|
||||
npm run build:local-https
|
||||
|
||||
# Build for production
|
||||
npm run build:production
|
||||
```
|
||||
|
||||
### Database Setup
|
||||
|
||||
1. **Import Schema**:
|
||||
```bash
|
||||
mysql -u username -p database_name < 0709.sql
|
||||
```
|
||||
|
||||
2. **Verify Tables**:
|
||||
- workers (user accounts)
|
||||
- clock_records (time tracking)
|
||||
- qr_codes (location codes)
|
||||
- location_updates (background tracking)
|
||||
- security_alerts (violations)
|
||||
- security_checks (assessments)
|
||||
|
||||
## 🔧 Development Workflow
|
||||
|
||||
### 1. Code Changes
|
||||
Make changes to Vue.js components in `src/` directory.
|
||||
|
||||
### 2. Test in Browser
|
||||
```bash
|
||||
npm run dev:all
|
||||
# Access at http://localhost:5173
|
||||
```
|
||||
|
||||
### 3. Build for Android
|
||||
```bash
|
||||
npm run build:android
|
||||
npm run open:android
|
||||
```
|
||||
|
||||
### 4. Test on Android
|
||||
- Use Android Studio emulator or physical device
|
||||
- Test background location functionality
|
||||
- Verify QR code scanning
|
||||
- Check security features
|
||||
|
||||
## 📱 Key Features
|
||||
|
||||
### Worker Features
|
||||
- **QR Code Clock In/Out**: Scan QR codes to record attendance
|
||||
- **Background Location Tracking**: Continuous monitoring while clocked in
|
||||
- **Attendance History**: View personal clock in/out records
|
||||
- **Multi-language Support**: English and other languages
|
||||
- **Offline Capability**: Works without internet connection
|
||||
|
||||
### Security Features
|
||||
- **GPS Spoofing Detection**: Detects fake GPS applications
|
||||
- **Device Validation**: UUID-based device registration
|
||||
- **Geofence Verification**: Server-side location validation
|
||||
- **Security Alerts**: Real-time violation logging
|
||||
|
||||
### Technical Features
|
||||
- **Native Android Integration**: Capacitor-based native features
|
||||
- **Background Services**: Location tracking while app is closed
|
||||
- **Secure Storage**: Encrypted local data storage
|
||||
- **API Integration**: RESTful backend communication
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
### Frontend Stack
|
||||
- **Vue 3**: Progressive JavaScript framework
|
||||
- **Capacitor**: Native runtime for web apps
|
||||
- **Tailwind CSS**: Utility-first CSS framework
|
||||
- **Vite**: Build tool and development server
|
||||
|
||||
### Backend Stack
|
||||
- **Node.js + Express**: REST API server
|
||||
- **MySQL 5.x**: Database with MyISAM engine
|
||||
- **JWT**: Authentication tokens
|
||||
- **bcrypt**: Password hashing
|
||||
|
||||
### Native Plugins
|
||||
- **Background Geolocation**: Location tracking
|
||||
- **Device Info**: Device identification
|
||||
- **Secure Storage**: Encrypted preferences
|
||||
- **Camera**: QR code scanning
|
||||
- **Local Notifications**: Background alerts
|
||||
|
||||
## 🔐 Security Implementation
|
||||
|
||||
### Anti-Spoofing Measures
|
||||
- Blacklist-based GPS spoofing app detection
|
||||
- Root/jailbreak detection
|
||||
- Device integrity verification
|
||||
- Real-time security monitoring
|
||||
|
||||
### Data Protection
|
||||
- JWT-based authentication
|
||||
- Encrypted local storage
|
||||
- HTTPS API communication
|
||||
- Input validation and sanitization
|
||||
|
||||
## 📊 Production Deployment
|
||||
|
||||
### Production API
|
||||
- **Endpoint**: https://myapp.ouji.com/nilai_clock_api/
|
||||
- **Environment**: Production server
|
||||
- **Database**: MySQL 5.x with optimized schema
|
||||
|
||||
### Android Release
|
||||
1. Build production version: `npm run build:production`
|
||||
2. Open Android Studio: `npm run open:android`
|
||||
3. Generate signed APK/AAB
|
||||
4. Deploy to Google Play Store
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
- **Background location stops**: Check battery optimization settings
|
||||
- **Camera permission denied**: Guide user through Android settings
|
||||
- **API connection fails**: Verify network configuration and endpoints
|
||||
|
||||
### Debug Commands
|
||||
```bash
|
||||
# Check build output
|
||||
npm run build
|
||||
|
||||
# Lint code
|
||||
npm run lint
|
||||
|
||||
# Format code
|
||||
npm run format
|
||||
```
|
||||
|
||||
## 📞 Support
|
||||
|
||||
### Development Environment
|
||||
- **Local IP**: 192.168.36.54 (true local IP)
|
||||
- **VPN IP**: 198.18.0.1 (TUN mode)
|
||||
- **Development Server**: Usually runs on port 5173
|
||||
- **Backend Server**: Usually runs on port 3000
|
||||
|
||||
### For iOS Development
|
||||
See [IOS_MIGRATION_GUIDE.md](./IOS_MIGRATION_GUIDE.md) for comprehensive iOS implementation guidance.
|
||||
|
||||
---
|
||||
|
||||
**Note**: This is a worker-only Android client application. Managers use the web interface for administrative functions. The app is designed for Android devices and requires specific native features that are not available on web platforms.
|
||||
@@ -21,6 +21,10 @@ class AntiSpoofingService {
|
||||
]
|
||||
}
|
||||
|
||||
// NOTE: This service performs simple anti-spoofing checks (blacklisted app detection)
|
||||
// and does NOT perform client-side security calculations or send security data to server.
|
||||
// Server-side security validation is preferred over client-side computation.
|
||||
|
||||
/**
|
||||
* Initialize the anti-spoofing service
|
||||
*/
|
||||
|
||||
@@ -63,24 +63,31 @@ class BackgroundLocationService {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
const messages = this.getNotificationMessages()
|
||||
|
||||
this.watcherId = await BackgroundGeolocation.addWatcher(
|
||||
{
|
||||
requestPermissions: true,
|
||||
stale: false,
|
||||
distanceFilter: 50,
|
||||
backgroundMessage: messages.message,
|
||||
backgroundTitle: messages.title
|
||||
},
|
||||
(location, error) => {
|
||||
if (error) {
|
||||
console.error('Background location error:', error)
|
||||
return
|
||||
try {
|
||||
this.watcherId = await BackgroundGeolocation.addWatcher(
|
||||
{
|
||||
requestPermissions: true,
|
||||
stale: false,
|
||||
distanceFilter: 50,
|
||||
backgroundMessage: messages.message,
|
||||
backgroundTitle: messages.title
|
||||
},
|
||||
(location, error) => {
|
||||
if (error) {
|
||||
console.error('Background location error:', error)
|
||||
return
|
||||
}
|
||||
if (location) {
|
||||
this.handleLocationUpdate(location)
|
||||
}
|
||||
}
|
||||
if (location) {
|
||||
this.handleLocationUpdate(location)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
console.log(`initialize: BackgroundGeolocation watcher added with ID: ${this.watcherId}`)
|
||||
} catch (watcherError) {
|
||||
console.error('initialize: Failed to add BackgroundGeolocation watcher:', watcherError)
|
||||
this.watcherId = null // Ensure watcherId is null on failure
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize network monitoring
|
||||
@@ -145,12 +152,27 @@ class BackgroundLocationService {
|
||||
}
|
||||
|
||||
if (this.watcherId) {
|
||||
console.log('startTracking: watcherId already exists, proceeding with tracking.')
|
||||
this.isTracking = true
|
||||
this.startPeriodicUpdates()
|
||||
return true
|
||||
}
|
||||
|
||||
throw new Error('Background location watcher not available')
|
||||
console.log('startTracking: watcherId is null, attempting to re-initialize.')
|
||||
const initialized = await this.initialize()
|
||||
if (initialized) {
|
||||
if (this.watcherId) { // Check again if initialize successfully set watcherId
|
||||
this.isTracking = true
|
||||
this.startPeriodicUpdates()
|
||||
return true
|
||||
} else {
|
||||
console.error('startTracking: Failed to re-initialize watcher, watcherId is still null.')
|
||||
throw new Error('Background location watcher not available after re-initialization attempt.')
|
||||
}
|
||||
} else {
|
||||
console.error('startTracking: Failed to initialize background location service during re-initialization attempt.')
|
||||
throw new Error('Failed to initialize background location service during re-initialization attempt.')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,9 +180,12 @@ class BackgroundLocationService {
|
||||
*/
|
||||
async stopTracking() {
|
||||
try {
|
||||
if (this.watcherId) {
|
||||
if (this.watcherId !== null) {
|
||||
console.log(`stopTracking: Removing watcher with ID: ${this.watcherId}`)
|
||||
await BackgroundGeolocation.removeWatcher({ id: this.watcherId })
|
||||
this.watcherId = null
|
||||
} else {
|
||||
console.log('stopTracking: watcherId is already null, no watcher to remove.')
|
||||
}
|
||||
|
||||
this.isTracking = false
|
||||
@@ -400,6 +425,7 @@ class BackgroundLocationService {
|
||||
await this.restoreClockStatus()
|
||||
|
||||
if (this.isClockedIn) {
|
||||
console.log('onAppResume: User is clocked in. Attempting to capture location and ensure tracking.')
|
||||
// Capture location on app resume
|
||||
try {
|
||||
await this.getCurrentLocationAndSend({
|
||||
@@ -407,16 +433,25 @@ class BackgroundLocationService {
|
||||
checkGeofence: true
|
||||
})
|
||||
} catch (locationError) {
|
||||
console.error('Failed to capture app resume location:', locationError)
|
||||
console.error('onAppResume: Failed to capture app resume location:', locationError)
|
||||
}
|
||||
|
||||
// Ensure tracking is active
|
||||
if (!this.isTracking) {
|
||||
await this.startTracking()
|
||||
console.log('onAppResume: Tracking is not active, attempting to start tracking.')
|
||||
try {
|
||||
await this.startTracking()
|
||||
} catch (startTrackingError) {
|
||||
console.error('onAppResume: Failed to start tracking on app resume:', startTrackingError)
|
||||
}
|
||||
} else {
|
||||
console.log('onAppResume: Tracking is already active.')
|
||||
}
|
||||
|
||||
// Sync pending data
|
||||
await this.syncPendingLocationData()
|
||||
} else {
|
||||
console.log('onAppResume: User is not clocked in. No action needed.')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to handle app resume:', error)
|
||||
|
||||
Reference in New Issue
Block a user