docs: 添加项目文档和优化后台服务

This commit is contained in:
sudomarcma
2025-07-11 14:07:20 +08:00
parent 4952f20528
commit 1a57c12749
9 changed files with 4086 additions and 74 deletions
+74 -53
View File
@@ -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) => {
+708
View File
@@ -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
+547
View File
@@ -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>"
```
+457
View File
@@ -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);
```
+878
View File
@@ -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
View File
@@ -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.
+4
View File
@@ -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
*/
+56 -21
View File
@@ -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)