8ecbd44948
- 添加反欺诈服务,定期检查设备上的黑名单应用 - 检测到黑名单应用时自动强制用户打卡下班 - 在登录后启动反欺诈检查,每5分钟执行一次 - 更新后端API支持强制打卡事件 - 调整Android最低SDK版本至24 - 添加全局事件监听处理强制打卡事件
1203 lines
42 KiB
JavaScript
1203 lines
42 KiB
JavaScript
import express from 'express'
|
|
import cors from 'cors'
|
|
import https from 'https'
|
|
import http from 'http'
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { fileURLToPath } from 'url'
|
|
|
|
import { Parser } from 'json2csv'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
import mysql from 'mysql2/promise'
|
|
import dotenv from 'dotenv'
|
|
import bcrypt from 'bcrypt'
|
|
import jwt from 'jsonwebtoken'
|
|
|
|
import { point, polygon, booleanPointInPolygon, pointToLineDistance } from '@turf/turf'
|
|
|
|
|
|
async function validateDeviceForUser(userId, deviceUuid, db) {
|
|
try {
|
|
// Step 1: Get user's current registered device UUID from workers table
|
|
const [userRows] = await db.execute(
|
|
'SELECT device_uuid, role, username FROM workers WHERE id = ?',
|
|
[userId]
|
|
)
|
|
|
|
if (userRows.length === 0) {
|
|
return {
|
|
valid: false,
|
|
message: 'User not found'
|
|
}
|
|
}
|
|
|
|
const user = userRows[0]
|
|
const registeredDeviceUuid = user.device_uuid
|
|
|
|
// Step 2: If no device is registered for this user (NULL device_uuid)
|
|
if (!registeredDeviceUuid) {
|
|
// Check if this device UUID is already registered to another user
|
|
const [otherUserRows] = await db.execute(
|
|
'SELECT id, username FROM workers WHERE device_uuid = ? AND id != ?',
|
|
[deviceUuid, userId]
|
|
)
|
|
|
|
if (otherUserRows.length > 0) {
|
|
// Log security alert for device conflict
|
|
await logSecurityAlert(userId, 'device_conflict', {
|
|
attempted_device_uuid: deviceUuid,
|
|
conflicting_user: otherUserRows[0].username,
|
|
message: 'Attempted to register device already assigned to another user'
|
|
}, db)
|
|
|
|
return {
|
|
valid: false,
|
|
message: 'Device is already registered to another account'
|
|
}
|
|
}
|
|
|
|
// Step 3: Auto-register this device for the user (initial setup)
|
|
await db.execute(
|
|
'UPDATE workers SET device_uuid = ? WHERE id = ?',
|
|
[deviceUuid, userId]
|
|
)
|
|
|
|
console.log(`Auto-registered device ${deviceUuid} for user ${userId} (${user.username}) - initial setup`)
|
|
return {
|
|
valid: true,
|
|
message: 'Device registered successfully for first-time login'
|
|
}
|
|
}
|
|
|
|
// Step 4: User has a registered device - check if current device matches
|
|
if (registeredDeviceUuid === deviceUuid) {
|
|
// Device UUID matches - allow login
|
|
return {
|
|
valid: true,
|
|
message: 'Device validated successfully'
|
|
}
|
|
}
|
|
|
|
// Step 5: Device UUID mismatch - log security alert and reject
|
|
await logSecurityAlert(userId, 'unauthorized_device_attempt', {
|
|
registered_device_uuid: registeredDeviceUuid,
|
|
attempted_device_uuid: deviceUuid,
|
|
username: user.username,
|
|
message: 'Login attempt from unauthorized device'
|
|
}, db)
|
|
|
|
return {
|
|
valid: false,
|
|
message: 'This device is not authorized for your account. Please use your registered device.'
|
|
}
|
|
} catch (error) {
|
|
console.error('Device validation error:', error)
|
|
return {
|
|
valid: false,
|
|
message: 'Device validation failed'
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to log security alerts for device validation issues
|
|
async function logSecurityAlert(userId, alertType, alertData, db) {
|
|
try {
|
|
await db.execute(
|
|
'INSERT INTO security_alerts (user_id, alert_type, alert_data, severity, created_at) VALUES (?, ?, ?, ?, NOW())',
|
|
[userId, alertType, JSON.stringify(alertData), 'medium']
|
|
)
|
|
console.log(`Security alert logged: ${alertType} for user ${userId}`)
|
|
} catch (error) {
|
|
console.error('Failed to log security alert:', error)
|
|
}
|
|
}
|
|
|
|
// Helper function to register a new device for user (simplified for workers table)
|
|
async function registerDeviceForUser(userId, deviceUuid, db) {
|
|
try {
|
|
// Check if device is already registered to another user
|
|
const [otherUserRows] = await db.execute(
|
|
'SELECT id, username FROM workers WHERE device_uuid = ? AND id != ?',
|
|
[deviceUuid, userId]
|
|
)
|
|
|
|
if (otherUserRows.length > 0) {
|
|
// Log security alert for device conflict
|
|
await logSecurityAlert(userId, 'device_registration_conflict', {
|
|
attempted_device_uuid: deviceUuid,
|
|
conflicting_user: otherUserRows[0].username,
|
|
message: 'Attempted to register device already assigned to another user'
|
|
}, db)
|
|
|
|
return {
|
|
success: false,
|
|
message: 'Device is already registered to another account'
|
|
}
|
|
}
|
|
|
|
// Register device by updating workers table
|
|
await db.execute(
|
|
'UPDATE workers SET device_uuid = ? WHERE id = ?',
|
|
[deviceUuid, userId]
|
|
)
|
|
|
|
console.log(`Device ${deviceUuid} registered for user ${userId}`)
|
|
return {
|
|
success: true,
|
|
message: 'Device registered successfully'
|
|
}
|
|
} catch (error) {
|
|
console.error('Device registration error:', error)
|
|
return {
|
|
success: false,
|
|
message: 'Database error during device registration'
|
|
}
|
|
}
|
|
}
|
|
|
|
// Main function to start the server
|
|
async function startServer() {
|
|
dotenv.config({ path: path.join(path.dirname(fileURLToPath(import.meta.url)), '.env') })
|
|
|
|
|
|
const app = express()
|
|
|
|
// --- Database Connection ---
|
|
const db = mysql.createPool({
|
|
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,
|
|
})
|
|
|
|
try {
|
|
const connection = await db.getConnection()
|
|
console.log('Database connected successfully!')
|
|
connection.release()
|
|
} catch (error) {
|
|
console.error('!!! DATABASE CONNECTION FAILED !!!')
|
|
console.error('Error:', error.message)
|
|
process.exit(1)
|
|
}
|
|
|
|
// Define the geofence polygon by calling the 'polygon' function directly
|
|
// const geofence = polygon([
|
|
// [
|
|
// [101.80827335908509, 2.8350045747358337],
|
|
// [101.80822799653066, 2.8340134829130363],
|
|
// [101.80827902940462, 2.8335264317641418],
|
|
// [101.80941309326164, 2.8332772427247335],
|
|
// [101.81144873788423, 2.834596811345506],
|
|
// [101.81166988033686, 2.8345911479647157],
|
|
// [101.81199875885511, 2.83593336858695],
|
|
// [101.80827335908509, 2.8350045747358337],
|
|
// ],
|
|
// ])
|
|
|
|
const geofence = polygon([
|
|
[
|
|
[113.310, 23.120],
|
|
[113.330, 23.120],
|
|
[113.310, 23.140],
|
|
[113.330, 23.140],
|
|
[113.310, 23.120]
|
|
]
|
|
])
|
|
|
|
|
|
// Enhanced CORS configuration for HTTPS and mobile development
|
|
const corsOptions = {
|
|
origin: function (origin, callback) {
|
|
// Allow requests with no origin (mobile apps, Postman, etc.)
|
|
if (!origin) return callback(null, true)
|
|
|
|
// Define allowed origins
|
|
const allowedOrigins = [
|
|
'http://localhost:5173',
|
|
'https://localhost:5173',
|
|
'capacitor://localhost',
|
|
'ionic://localhost',
|
|
'http://localhost',
|
|
'https://localhost'
|
|
]
|
|
|
|
// Add environment-specific origins
|
|
if (process.env.CORS_ORIGIN) {
|
|
const envOrigins = process.env.CORS_ORIGIN.split(',')
|
|
allowedOrigins.push(...envOrigins)
|
|
}
|
|
|
|
// Add local IP origins for mobile testing
|
|
if (process.env.SERVER_IP) {
|
|
const serverIP = process.env.SERVER_IP
|
|
allowedOrigins.push(
|
|
`http://${serverIP}:5173`,
|
|
`https://${serverIP}:5173`,
|
|
`http://${serverIP}:3000`,
|
|
`https://${serverIP}:3443`
|
|
)
|
|
}
|
|
|
|
// Check if origin is allowed
|
|
if (allowedOrigins.indexOf(origin) !== -1 || origin.startsWith('capacitor://') || origin.startsWith('ionic://')) {
|
|
callback(null, true)
|
|
} else {
|
|
console.log('CORS blocked origin:', origin)
|
|
callback(null, true) // Allow all origins in development
|
|
}
|
|
},
|
|
credentials: true,
|
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
allowedHeaders: ['Content-Type', 'Authorization', 'ngrok-skip-browser-warning'],
|
|
exposedHeaders: ['Content-Range', 'X-Content-Range']
|
|
}
|
|
|
|
app.use(cors(corsOptions))
|
|
app.use(express.json())
|
|
|
|
// --- API Endpoints ---
|
|
|
|
// Auth Endpoint with Device UUID validation
|
|
app.post('/api/auth/login', async (req, res) => {
|
|
try {
|
|
const { username, password, deviceUuid } = req.body
|
|
const [rows] = await db.execute(
|
|
'SELECT id, role, password_hash FROM workers WHERE username = ?',
|
|
[username],
|
|
)
|
|
if (rows.length > 0) {
|
|
const user = rows[0]
|
|
const passwordMatch = await bcrypt.compare(password, user.password_hash)
|
|
if (passwordMatch) {
|
|
// Check device UUID validation if provided, but skip for managers
|
|
if (deviceUuid && user.role !== 'manager') {
|
|
const deviceValidation = await validateDeviceForUser(user.id, deviceUuid, db)
|
|
if (!deviceValidation.valid) {
|
|
return res.status(403).json({
|
|
message: 'Device not authorized for this account',
|
|
deviceError: deviceValidation.message
|
|
})
|
|
}
|
|
}
|
|
|
|
const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, {
|
|
expiresIn: '1h',
|
|
})
|
|
res.json({ message: 'Login successful', token })
|
|
} else {
|
|
res.status(401).json({ message: 'Invalid credentials' })
|
|
}
|
|
} else {
|
|
res.status(401).json({ message: 'Invalid credentials' })
|
|
}
|
|
} catch (error) {
|
|
console.error('Login error:', error)
|
|
res.status(500).json({ message: 'Database error during login.' })
|
|
}
|
|
})
|
|
|
|
// Middleware to verify JWT
|
|
const authenticateJWT = (req, res, next) => {
|
|
const authHeader = req.headers.authorization
|
|
|
|
if (authHeader) {
|
|
const token = authHeader.split(' ')[1]
|
|
|
|
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
|
|
if (err) {
|
|
return res.status(403).json({ message: 'Forbidden' })
|
|
}
|
|
|
|
req.user = user
|
|
next()
|
|
})
|
|
} else {
|
|
res.status(401).json({ message: 'Unauthorized' })
|
|
}
|
|
}
|
|
|
|
// Worker Clock In/Out Endpoint
|
|
app.post('/api/clock', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { userId, eventType, qrCodeValue, latitude, longitude, notes } = req.body
|
|
|
|
// Bypass geofence and QR code validation for forced events
|
|
if (qrCodeValue !== 'FORCE_CLOCK_OUT' && qrCodeValue !== 'BLACKLIST_APP_DETECTED') {
|
|
// Geofencing check using the directly imported functions
|
|
const userLocation = point([longitude, latitude]);
|
|
const isWithinGeofence = booleanPointInPolygon(userLocation, geofence);
|
|
|
|
if (!isWithinGeofence) {
|
|
// User is outside the geofence, log a 'failed' attempt
|
|
const distance = pointToLineDistance(userLocation, geofence.geometry.coordinates[0], { units: 'meters' });
|
|
const notes = `Clock-in outside of the zone: ${distance.toFixed(2)} meters.`;
|
|
|
|
await db.execute(
|
|
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
[userId, 'failed', new Date(), qrCodeValue, latitude, longitude, notes]
|
|
);
|
|
|
|
return res.status(403).json({ message: `You are not within the allowed work area.` });
|
|
}
|
|
|
|
const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [
|
|
qrCodeValue,
|
|
]);
|
|
|
|
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'}.` })
|
|
}
|
|
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, latitude, longitude, notes],
|
|
)
|
|
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.' })
|
|
}
|
|
})
|
|
|
|
// Fetch worker details endpoint
|
|
app.get('/api/workers/:id', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { id } = req.params
|
|
const [rows] = await db.execute(
|
|
'SELECT full_name FROM workers WHERE id = ? AND role = "worker"',
|
|
[id],
|
|
)
|
|
if (rows.length > 0) {
|
|
res.json({ full_name: rows[0].full_name })
|
|
} else {
|
|
res.status(404).json({ message: 'Worker not found.' })
|
|
}
|
|
} catch (error) {
|
|
console.error('Get worker details error:', error)
|
|
res.status(500).json({ message: 'Database error fetching worker details.' })
|
|
}
|
|
})
|
|
|
|
// Worker Status Endpoint
|
|
app.get('/api/worker/status/:userId', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { userId } = req.params
|
|
const [rows] = await db.execute(
|
|
'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1',
|
|
[userId],
|
|
)
|
|
if (rows.length > 0) {
|
|
res.json({ eventType: rows[0].event_type })
|
|
} else {
|
|
res.json({ eventType: 'clock_out' }) // Default to clocked out
|
|
}
|
|
} catch (error) {
|
|
console.error('Worker status error:', error)
|
|
res.status(500).json({ message: 'Database error fetching status.' })
|
|
}
|
|
})
|
|
|
|
// Worker History Endpoint
|
|
app.get('/api/worker/clock-history/:userId', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { userId } = req.params
|
|
// MODIFIED: Use LEFT JOIN and COALESCE to handle manual entries
|
|
const [rows] = await db.execute(
|
|
`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`,
|
|
[userId],
|
|
)
|
|
res.json(rows)
|
|
} catch (error) {
|
|
console.error('Worker history error:', error)
|
|
res.status(500).json({ message: 'Database error fetching history.' })
|
|
}
|
|
})
|
|
|
|
app.put('/api/worker/change-password', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { userId } = req.user // Get user ID from JWT
|
|
const { currentPassword, newPassword } = req.body
|
|
|
|
if (!currentPassword || !newPassword) {
|
|
return res.status(400).json({ message: 'Current password and new password are required.' })
|
|
}
|
|
if (newPassword.length < 6) {
|
|
return res.status(400).json({ message: 'New password must be at least 6 characters long.' })
|
|
}
|
|
|
|
// Get user's current password hash
|
|
const [rows] = await db.execute('SELECT password_hash FROM workers WHERE id = ?', [userId])
|
|
|
|
if (rows.length === 0) {
|
|
return res.status(404).json({ message: 'User not found.' })
|
|
}
|
|
|
|
const user = rows[0]
|
|
|
|
// Verify current password
|
|
const passwordMatch = await bcrypt.compare(currentPassword, user.password_hash)
|
|
if (!passwordMatch) {
|
|
return res.status(401).json({ message: 'Incorrect current password.' })
|
|
}
|
|
|
|
// Hash new password
|
|
const saltRounds = 10
|
|
const newHashedPassword = await bcrypt.hash(newPassword, saltRounds)
|
|
|
|
// Update password in DB
|
|
await db.execute('UPDATE workers SET password_hash = ? WHERE id = ?', [
|
|
newHashedPassword,
|
|
userId,
|
|
])
|
|
|
|
res.json({ message: 'Password updated successfully.' })
|
|
} catch (error) {
|
|
console.error('Change password error:', error)
|
|
res.status(500).json({ message: 'Database error during password change.' })
|
|
}
|
|
})
|
|
|
|
// Manager: PUT (Update) a Worker's Password
|
|
app.put('/api/managers/workers/:workerId/password', authenticateJWT, async (req, res) => {
|
|
try {
|
|
// Ensure the user performing the action is a manager
|
|
if (req.user.role !== 'manager') {
|
|
return res
|
|
.status(403)
|
|
.json({ message: 'Forbidden: You do not have permission to perform this action.' })
|
|
}
|
|
|
|
const { workerId } = req.params
|
|
const { newPassword } = req.body
|
|
|
|
if (!newPassword || newPassword.length < 6) {
|
|
return res.status(400).json({ message: 'Password must be at least 6 characters long.' })
|
|
}
|
|
|
|
const saltRounds = 10
|
|
const hashedPassword = await bcrypt.hash(newPassword, saltRounds)
|
|
|
|
const [result] = await db.execute(
|
|
"UPDATE workers SET password_hash = ? WHERE id = ? AND role = 'worker'",
|
|
[hashedPassword, workerId],
|
|
)
|
|
|
|
if (result.affectedRows === 0) {
|
|
return res
|
|
.status(404)
|
|
.json({ message: 'Worker not found or you cannot change the password for this user.' })
|
|
}
|
|
|
|
res.status(200).json({ message: 'Password updated successfully.' })
|
|
} catch (error) {
|
|
console.error('Update password error:', error)
|
|
res.status(500).json({ message: 'Database error while updating password.' })
|
|
}
|
|
})
|
|
// GET all tags
|
|
app.get('/api/managers/tags', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const [tags] = await db.execute('SELECT * FROM tags ORDER BY tag_name ASC')
|
|
res.json(tags)
|
|
} catch (error) {
|
|
console.error('Get tags error:', error)
|
|
res.status(500).json({ message: 'Database error fetching tags.' })
|
|
}
|
|
})
|
|
|
|
// POST a new tag
|
|
app.post('/api/managers/tags', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { tag_name } = req.body
|
|
if (!tag_name) {
|
|
return res.status(400).json({ message: 'Tag name is required.' })
|
|
}
|
|
const [result] = await db.execute('INSERT INTO tags (tag_name) VALUES (?)', [tag_name])
|
|
res.status(201).json({ id: result.insertId, tag_name })
|
|
} catch (error) {
|
|
if (error.code === 'ER_DUP_ENTRY') {
|
|
return res.status(409).json({ message: 'This tag already exists.' })
|
|
}
|
|
console.error('Add tag error:', error)
|
|
res.status(500).json({ message: 'Database error adding tag.' })
|
|
}
|
|
})
|
|
|
|
// NEW: DELETE a tag
|
|
app.delete('/api/managers/tags/:id', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { id } = req.params
|
|
|
|
// Optional: Check if the user is a manager before allowing deletion
|
|
if (req.user.role !== 'manager') {
|
|
return res.status(403).json({ message: 'Forbidden: Only managers can delete tags.' })
|
|
}
|
|
|
|
// Delete the tag from the 'tags' table.
|
|
// If 'worker_tags' table has ON DELETE CASCADE for tag_id,
|
|
// related entries in 'worker_tags' will automatically be removed.
|
|
const [result] = await db.execute('DELETE FROM tags WHERE id = ?', [id])
|
|
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ message: 'Tag not found.' })
|
|
}
|
|
|
|
res.status(204).send() // 204 No Content for successful deletion
|
|
} catch (error) {
|
|
console.error('Delete tag error:', error)
|
|
res.status(500).json({ message: 'Database error deleting tag.' })
|
|
}
|
|
})
|
|
|
|
// POST to assign a tag to a worker
|
|
app.post('/api/managers/workers/:workerId/tags', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { workerId } = req.params
|
|
const { tagId } = req.body // Expects a single tag ID
|
|
|
|
if (!tagId) {
|
|
return res.status(400).json({ message: 'Tag ID is required.' })
|
|
}
|
|
|
|
// INSERT IGNORE prevents errors if the tag is already assigned to the worker
|
|
await db.query('INSERT IGNORE INTO worker_tags (worker_id, tag_id) VALUES (?, ?)', [
|
|
workerId,
|
|
tagId,
|
|
])
|
|
|
|
res.status(200).json({ message: 'Tag assigned successfully.' })
|
|
} catch (error) {
|
|
console.error('Assign tag error:', error)
|
|
res.status(500).json({ message: 'Database error assigning tag.' })
|
|
}
|
|
})
|
|
|
|
// DELETE to remove a tag from a worker
|
|
app.delete('/api/managers/workers/:workerId/tags/:tagId', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { workerId, tagId } = req.params
|
|
await db.query('DELETE FROM worker_tags WHERE worker_id = ? AND tag_id = ?', [
|
|
workerId,
|
|
tagId,
|
|
])
|
|
res.status(204).send() // 204 No Content for successful deletion
|
|
} catch (error) {
|
|
console.error('Remove tag error:', error)
|
|
res.status(500).json({ message: 'Database error removing tag.' })
|
|
}
|
|
})
|
|
|
|
// Find this endpoint in your server.js and replace it with the code below.
|
|
|
|
// Manager: GET All Workers (FIXED for older MySQL versions)
|
|
app.get('/api/managers/workers', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { search = '', page = 1, limit = 20, tags = '' } = req.query
|
|
const offset = (parseInt(page) - 1) * parseInt(limit)
|
|
const searchTerm = `%${search}%`
|
|
|
|
const tagIds = tags
|
|
.split(',')
|
|
.filter((id) => id)
|
|
.map(Number)
|
|
const hasTagFilter = tagIds.length > 0
|
|
|
|
// Base queries
|
|
let baseQuery = `
|
|
SELECT
|
|
w.id, w.username, w.full_name, w.created_at,
|
|
(SELECT GROUP_CONCAT(t.tag_name SEPARATOR ', ')
|
|
FROM worker_tags wt_sub
|
|
JOIN tags t ON wt_sub.tag_id = t.id
|
|
WHERE wt_sub.worker_id = w.id) as tags
|
|
FROM workers w
|
|
`
|
|
let countQuery = `SELECT COUNT(DISTINCT w.id) as totalCount FROM workers w`
|
|
|
|
// Parameters for the queries
|
|
const params = []
|
|
const countParams = []
|
|
|
|
// Join with worker_tags if filtering
|
|
if (hasTagFilter) {
|
|
const joinClause = ` JOIN worker_tags wt ON w.id = wt.worker_id`
|
|
baseQuery += joinClause
|
|
countQuery += joinClause
|
|
}
|
|
|
|
// Common WHERE clause
|
|
const whereClause = ` WHERE w.role = 'worker' AND (w.full_name LIKE ? OR w.username LIKE ?)`
|
|
baseQuery += whereClause
|
|
countQuery += whereClause
|
|
params.push(searchTerm, searchTerm)
|
|
countParams.push(searchTerm, searchTerm)
|
|
|
|
// Add tag filtering logic
|
|
if (hasTagFilter) {
|
|
const tagPlaceholders = tagIds.map(() => '?').join(',')
|
|
|
|
const tagFilterClause = ` AND wt.tag_id IN (${tagPlaceholders})`
|
|
baseQuery += tagFilterClause
|
|
countQuery += tagFilterClause
|
|
|
|
// Add the tag IDs to the parameters individually
|
|
params.push(...tagIds)
|
|
countParams.push(...tagIds)
|
|
// --- FIX END ---
|
|
}
|
|
|
|
// Grouping and pagination for the main query
|
|
if (hasTagFilter) {
|
|
baseQuery += ` GROUP BY w.id HAVING COUNT(DISTINCT wt.tag_id) = ?`
|
|
params.push(tagIds.length)
|
|
}
|
|
|
|
baseQuery += ` ORDER BY w.created_at DESC LIMIT ? OFFSET ?`
|
|
params.push(parseInt(limit), offset)
|
|
|
|
// Execute queries
|
|
const [workers] = await db.execute(baseQuery, params)
|
|
const [[{ totalCount }]] = await db.execute(countQuery, countParams)
|
|
|
|
res.json({ workers, totalCount })
|
|
} catch (error) {
|
|
// This is the error you are seeing
|
|
console.error('Get workers error:', error)
|
|
res.status(500).json({ message: 'Database error fetching workers.' })
|
|
}
|
|
})
|
|
|
|
// Manager: POST (Add new) Worker
|
|
|
|
app.post('/api/managers/workers', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { username, password, fullName, role = 'worker' } = req.body
|
|
if (!username || !password || !fullName) {
|
|
return res.status(400).json({ message: 'Username, password, and full name are required.' })
|
|
}
|
|
|
|
if (!['worker', 'manager'].includes(role)) {
|
|
return res.status(400).json({ message: 'Invalid role specified.' })
|
|
}
|
|
const saltRounds = 10
|
|
const hashedPassword = await bcrypt.hash(password, saltRounds)
|
|
|
|
const [result] = await db.execute(
|
|
'INSERT INTO workers (username, password_hash, full_name, role) VALUES (?, ?, ?, ?)',
|
|
[username, hashedPassword, fullName, role], // Pass role to query
|
|
)
|
|
res.status(201).json({ id: result.insertId, username, fullName, role })
|
|
} catch (error) {
|
|
if (error.code === 'ER_DUP_ENTRY') {
|
|
return res.status(409).json({ message: 'Username already exists.' })
|
|
}
|
|
console.error('Add worker error:', error)
|
|
res.status(500).json({ message: 'Database error adding worker.' })
|
|
}
|
|
})
|
|
|
|
// Manager: DELETE Worker
|
|
app.delete('/api/managers/workers/:id', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { id } = req.params
|
|
const [result] = await db.execute("DELETE FROM workers WHERE id = ? AND role = 'worker'", [
|
|
id,
|
|
])
|
|
if (result.affectedRows === 0) {
|
|
return res.status(404).json({ message: 'Worker not found or user is not a worker.' })
|
|
}
|
|
res.status(204).send()
|
|
} catch (error) {
|
|
console.error('Delete worker error:', error)
|
|
res.status(500).json({ message: 'Database error deleting worker.' })
|
|
}
|
|
})
|
|
|
|
// --- NEW --- Manager: POST (Add Manual Attendance Record)
|
|
// Note: For this to work, you may need to alter your database table:
|
|
// ALTER TABLE clock_records ADD COLUMN notes TEXT;
|
|
app.post('/api/managers/add-record', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { workerId, eventType, timestamp, notes } = req.body
|
|
|
|
if (!workerId || !eventType || !timestamp) {
|
|
return res
|
|
.status(400)
|
|
.json({ message: 'Worker ID, event type, and timestamp are required.' })
|
|
}
|
|
|
|
// Check last event to prevent adding a duplicate event type
|
|
const [lastEventRows] = await db.execute(
|
|
'SELECT event_type FROM clock_records WHERE worker_id = ? ORDER BY timestamp DESC LIMIT 1',
|
|
[workerId],
|
|
)
|
|
|
|
if (lastEventRows.length > 0 && lastEventRows[0].event_type === eventType) {
|
|
const status = eventType === 'clock_in' ? 'in' : 'out'
|
|
return res.status(409).json({ message: `Worker is already clocked ${status}.` })
|
|
}
|
|
// --- THIS IS THE FIX ---
|
|
const sanitizedTimestamp = timestamp.replace('T', ' ')
|
|
|
|
await db.execute(
|
|
'INSERT INTO clock_records (worker_id, event_type, timestamp, notes, qr_code_id, latitude, longitude) VALUES (?, ?, ?, ?, NULL, NULL, NULL)',
|
|
[workerId, eventType, sanitizedTimestamp, notes],
|
|
)
|
|
|
|
res.status(201).json({ message: 'Manual record added successfully.' })
|
|
} catch (error) {
|
|
console.error('Add manual record error:', error)
|
|
res.status(500).json({ message: 'Database error adding manual record.' })
|
|
}
|
|
})
|
|
|
|
// Manager: GET Attendance Records
|
|
app.get('/api/managers/attendance-records', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { workerIds, startDate, endDate, format } = req.query
|
|
if (!workerIds) {
|
|
return res.status(400).json({ message: 'Worker IDs are required.' })
|
|
}
|
|
const idsArray = workerIds.split(',').map(Number)
|
|
if (idsArray.length === 0) return res.json([])
|
|
const placeholders = idsArray.map(() => '?').join(',')
|
|
|
|
// MODIFIED: Use LEFT JOIN and COALESCE to handle manual entries, and select `notes`
|
|
let query = `SELECT cr.id, w.full_name, cr.event_type, cr.timestamp, COALESCE(qc.name, 'Manual Entry') as qrCodeUsedName, cr.latitude, cr.longitude, cr.notes FROM clock_records cr LEFT JOIN qr_codes qc ON cr.qr_code_id = qc.id JOIN workers w ON cr.worker_id = w.id WHERE cr.worker_id IN (${placeholders})`;
|
|
|
|
const params = [...idsArray]
|
|
if (startDate && endDate) {
|
|
const endOfDay = new Date(endDate)
|
|
endOfDay.setHours(23, 59, 59, 999)
|
|
query += ' AND cr.timestamp BETWEEN ? AND ?'
|
|
params.push(startDate, endOfDay)
|
|
}
|
|
query += ' ORDER BY w.full_name, cr.timestamp DESC'
|
|
|
|
const [rows] = await db.execute(query, params)
|
|
|
|
if (format === 'csv') {
|
|
// MODIFIED: Add 'notes' to CSV export
|
|
const json2csvParser = new Parser({
|
|
fields: ['full_name', 'event_type', 'timestamp', 'qrCodeUsedName', 'notes'],
|
|
})
|
|
const csv = json2csvParser.parse(rows)
|
|
res.header('Content-Type', 'text/csv')
|
|
res.attachment(`attendance-report-${new Date().toISOString().split('T')[0]}.csv`)
|
|
return res.send(csv)
|
|
}
|
|
res.json(rows)
|
|
} catch (error) {
|
|
console.error('Attendance records error:', error)
|
|
res.status(500).json({ message: 'Database error fetching attendance records.' })
|
|
}
|
|
})
|
|
|
|
// Manager: GET QR Codes
|
|
app.get('/api/managers/qr-codes', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const [rows] = await db.execute(
|
|
'SELECT id, name, is_active, created_at FROM qr_codes ORDER BY created_at DESC',
|
|
)
|
|
res.json(rows)
|
|
} catch (error) {
|
|
console.error('Get QR codes error:', error)
|
|
res.status(500).json({ message: 'Database error fetching QR codes.' })
|
|
}
|
|
})
|
|
|
|
// Manager: POST QR Code
|
|
app.post('/api/managers/qr-codes', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { name } = req.body
|
|
if (!name) return res.status(400).json({ message: 'QR Code name is required.' })
|
|
const newQrCode = { id: uuidv4(), name, isActive: true }
|
|
await db.execute('INSERT INTO qr_codes (id, name, is_active) VALUES (?, ?, ?)', [
|
|
newQrCode.id,
|
|
newQrCode.name,
|
|
newQrCode.isActive,
|
|
])
|
|
res
|
|
.status(201)
|
|
.json({ id: newQrCode.id, name: newQrCode.name, is_active: newQrCode.isActive })
|
|
} catch (error) {
|
|
console.error('Add QR code error:', error)
|
|
res.status(500).json({ message: 'Database error adding QR code.' })
|
|
}
|
|
})
|
|
|
|
// Manager: PUT QR Code
|
|
app.put('/api/managers/qr-codes/:id', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { id } = req.params
|
|
const { isActive } = req.body
|
|
if (typeof isActive !== 'boolean')
|
|
return res.status(400).json({ message: 'isActive must be a boolean.' })
|
|
const [result] = await db.execute('UPDATE qr_codes SET is_active = ? WHERE id = ?', [
|
|
isActive,
|
|
id,
|
|
])
|
|
if (result.affectedRows === 0) return res.status(404).json({ message: 'QR Code not found.' })
|
|
res.json({ id, isActive })
|
|
} catch (error) {
|
|
console.error('Update QR code error:', error)
|
|
res.status(500).json({ message: 'Database error updating QR code.' })
|
|
}
|
|
})
|
|
|
|
// Manager: DELETE QR Code
|
|
app.delete('/api/managers/qr-codes/:id', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { id } = req.params
|
|
const [result] = await db.execute('DELETE FROM qr_codes WHERE id = ?', [id])
|
|
if (result.affectedRows === 0) return res.status(404).json({ message: 'QR Code not found.' })
|
|
res.status(204).send()
|
|
} catch (error) {
|
|
console.error('Delete QR code error:', error)
|
|
res.status(500).json({ message: 'Database error deleting QR code.' })
|
|
}
|
|
})
|
|
|
|
// Manager: GET single worker's details
|
|
app.get('/api/managers/worker/:id', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { id } = req.params
|
|
const [rows] = await db.execute(
|
|
"SELECT full_name FROM workers WHERE id = ? AND role = 'worker'",
|
|
[id],
|
|
)
|
|
if (rows.length > 0) {
|
|
res.json(rows[0])
|
|
} else {
|
|
res.status(404).json({ message: 'Worker not found.' })
|
|
}
|
|
} catch (error) {
|
|
console.error('Get single worker error:', error)
|
|
res.status(500).json({ message: 'Database error fetching worker details.' })
|
|
}
|
|
})
|
|
|
|
// --- NEW NATIVE FEATURES API ENDPOINTS ---
|
|
|
|
// Location Update Endpoint - OPTIMIZED: Removed continuous geofence checking
|
|
app.post('/api/location/update', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { userId, latitude, longitude, checkGeofence } = req.body
|
|
|
|
if (!userId || !latitude || !longitude) {
|
|
return res.status(400).json({ message: 'User ID, latitude, and longitude are required.' })
|
|
}
|
|
|
|
// OPTIMIZATION: Only check geofence when explicitly requested
|
|
// This reduces unnecessary processing on every location update
|
|
if (checkGeofence === true) {
|
|
try {
|
|
const userLocation = point([longitude, latitude]);
|
|
const isWithinGeofence = booleanPointInPolygon(userLocation, geofence);
|
|
|
|
if (!isWithinGeofence) {
|
|
// User is outside the geofence - log security alert silently
|
|
const distance = pointToLineDistance(userLocation, geofence.geometry.coordinates[0], { units: 'meters' });
|
|
|
|
const alertData = {
|
|
latitude: latitude,
|
|
longitude: longitude,
|
|
timestamp: new Date().toISOString(),
|
|
distance_from_geofence: distance.toFixed(2),
|
|
check_type: 'scheduled_update' // Indicate this was a scheduled check
|
|
};
|
|
|
|
// Log geofence violation to security_alerts table
|
|
await logSecurityAlert(userId, 'geofence_violation', alertData, db);
|
|
|
|
console.log(`OPTIMIZED: Geofence violation detected for user ${userId}: ${distance.toFixed(2)} meters outside boundary`);
|
|
} else {
|
|
console.log(`OPTIMIZED: User ${userId} within geofence during scheduled check`);
|
|
}
|
|
} catch (geofenceError) {
|
|
console.error('OPTIMIZED: Error checking geofence:', geofenceError);
|
|
// Continue with location update even if geofence check fails
|
|
}
|
|
}
|
|
|
|
// OPTIMIZED: Simplified location update - only essential fields
|
|
// No need for timestamp conversion as we use created_at with NOW()
|
|
await db.execute(
|
|
'INSERT INTO location_updates (user_id, longitude, latitude, created_at) VALUES (?, ?, ?, NOW())',
|
|
[userId, longitude, latitude]
|
|
)
|
|
|
|
res.json({ message: 'Location updated successfully' })
|
|
} catch (error) {
|
|
console.error('Location update error:', error)
|
|
res.status(500).json({ message: 'Database error during location update.' })
|
|
}
|
|
})
|
|
|
|
// Device Registration Endpoint
|
|
app.post('/api/device/register', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { userId, deviceUuid, deviceInfo } = req.body
|
|
|
|
if (!userId || !deviceUuid) {
|
|
return res.status(400).json({ message: 'User ID and device UUID are required.' })
|
|
}
|
|
|
|
const result = await registerDeviceForUser(userId, deviceUuid, deviceInfo, db)
|
|
|
|
if (result.success) {
|
|
res.json({ message: result.message, success: true })
|
|
} else {
|
|
res.status(409).json({ message: result.message, success: false })
|
|
}
|
|
} catch (error) {
|
|
console.error('Device registration error:', error)
|
|
res.status(500).json({ message: 'Database error during device registration.', success: false })
|
|
}
|
|
})
|
|
|
|
// Device Validation Endpoint
|
|
app.post('/api/device/validate', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { userId, deviceUuid } = req.body
|
|
|
|
if (!userId || !deviceUuid) {
|
|
return res.status(400).json({ message: 'User ID and device UUID are required.' })
|
|
}
|
|
|
|
const validation = await validateDeviceForUser(userId, deviceUuid, db)
|
|
|
|
res.json({
|
|
valid: validation.valid,
|
|
message: validation.message
|
|
})
|
|
} catch (error) {
|
|
console.error('Device validation error:', error)
|
|
res.status(500).json({ message: 'Database error during device validation.', valid: false })
|
|
}
|
|
})
|
|
|
|
|
|
// Security Check Endpoint
|
|
app.post('/api/security/check', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { userId, timestamp, deviceInfo, securityCheck } = req.body
|
|
|
|
if (!userId || !securityCheck) {
|
|
return res.status(400).json({ message: 'User ID and security check data are required.' })
|
|
}
|
|
|
|
// Calculate risk score
|
|
let riskScore = 0
|
|
if (securityCheck.suspiciousApps && securityCheck.suspiciousApps.totalSuspiciousApps > 0) {
|
|
riskScore += securityCheck.suspiciousApps.totalSuspiciousApps * 10
|
|
}
|
|
if (securityCheck.riskLevel === 'high') {
|
|
riskScore += 50
|
|
} else if (securityCheck.riskLevel === 'medium') {
|
|
riskScore += 25
|
|
}
|
|
|
|
// Convert timestamp to MySQL-compatible format
|
|
let mysqlTimestamp
|
|
if (timestamp) {
|
|
if (typeof timestamp === 'string' && timestamp.includes('T')) {
|
|
// Handle ISO 8601 format (e.g., '2025-07-04T09:00:49.192Z')
|
|
// Convert to MySQL DATETIME format by replacing 'T' with ' ' and removing 'Z'
|
|
mysqlTimestamp = timestamp.replace('T', ' ').replace('Z', '')
|
|
} else if (timestamp instanceof Date) {
|
|
// Handle Date object
|
|
mysqlTimestamp = timestamp.toISOString().replace('T', ' ').replace('Z', '')
|
|
} else {
|
|
// Fallback to current time for invalid formats
|
|
mysqlTimestamp = new Date().toISOString().replace('T', ' ').replace('Z', '')
|
|
}
|
|
} else {
|
|
// Use current time if no timestamp provided
|
|
mysqlTimestamp = new Date().toISOString().replace('T', ' ').replace('Z', '')
|
|
}
|
|
|
|
// Store security check results
|
|
await db.execute(
|
|
'INSERT INTO security_checks (user_id, timestamp, device_info, security_data, risk_level, risk_score, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())',
|
|
[userId, mysqlTimestamp, JSON.stringify(deviceInfo), JSON.stringify(securityCheck), securityCheck.riskLevel, riskScore]
|
|
)
|
|
|
|
// Check if risk level is too high
|
|
if (riskScore >= 50) {
|
|
// Log high-risk event
|
|
await db.execute(
|
|
'INSERT INTO security_alerts (user_id, alert_type, alert_data, severity, created_at) VALUES (?, ?, ?, ?, NOW())',
|
|
[userId, 'high_risk_device', JSON.stringify({ riskScore, securityCheck }), 'high']
|
|
)
|
|
|
|
return res.status(403).json({
|
|
message: 'Device security risk too high. Please contact administrator.',
|
|
riskLevel: securityCheck.riskLevel,
|
|
riskScore: riskScore
|
|
})
|
|
}
|
|
|
|
res.json({
|
|
message: 'Security check completed successfully',
|
|
riskLevel: securityCheck.riskLevel,
|
|
riskScore: riskScore,
|
|
status: 'approved'
|
|
})
|
|
} catch (error) {
|
|
console.error('Security check error:', error)
|
|
res.status(500).json({ message: 'Database error during security check.' })
|
|
}
|
|
})
|
|
|
|
// Get Security Status Endpoint
|
|
app.get('/api/security/status/:userId', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const { userId } = req.params
|
|
|
|
// Get latest security check
|
|
const [securityRows] = await db.execute(
|
|
'SELECT * FROM security_checks WHERE user_id = ? ORDER BY created_at DESC LIMIT 1',
|
|
[userId]
|
|
)
|
|
|
|
// Get recent security alerts
|
|
const [alertRows] = await db.execute(
|
|
'SELECT * FROM security_alerts WHERE user_id = ? AND created_at > DATE_SUB(NOW(), INTERVAL 7 DAY) ORDER BY created_at DESC',
|
|
[userId]
|
|
)
|
|
|
|
res.json({
|
|
latestSecurityCheck: securityRows[0] || null,
|
|
recentAlerts: alertRows,
|
|
securityStatus: securityRows.length > 0 ? securityRows[0].risk_level : 'unknown'
|
|
})
|
|
} catch (error) {
|
|
console.error('Get security status error:', error)
|
|
res.status(500).json({ message: 'Database error fetching security status.' })
|
|
}
|
|
})
|
|
|
|
// Get App Blacklist Endpoint
|
|
app.get('/api/security/app-blacklist', authenticateJWT, async (req, res) => {
|
|
try {
|
|
const [rows] = await db.execute('SELECT package_name FROM app_blacklist');
|
|
const packageNames = rows.map(row => row.package_name);
|
|
res.json(packageNames);
|
|
} catch (error) {
|
|
console.error('Get app blacklist error:', error);
|
|
res.status(500).json({ message: 'Database error fetching app blacklist.' });
|
|
}
|
|
});
|
|
|
|
// --- Server Start ---
|
|
const httpPort = process.env.HTTP_PORT || 3000
|
|
const httpsPort = process.env.HTTPS_PORT || 3443
|
|
const sslEnabled = process.env.SSL_ENABLED === 'true'
|
|
|
|
if (sslEnabled) {
|
|
try {
|
|
// Check if SSL certificate files exist
|
|
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
|
const keyPath = path.join(currentDir, 'key.pem')
|
|
const certPath = path.join(currentDir, 'cert.pem')
|
|
|
|
console.log('Resolved keyPath:', keyPath)
|
|
console.log('Resolved certPath:', certPath)
|
|
|
|
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
|
|
console.error('SSL certificate or key file not found. Falling back to HTTP.')
|
|
startHttpServer()
|
|
return
|
|
}
|
|
|
|
const httpsOptions = {
|
|
key: fs.readFileSync(keyPath),
|
|
cert: fs.readFileSync(certPath),
|
|
}
|
|
|
|
// Start HTTPS server
|
|
const httpsServer = https.createServer(httpsOptions, app)
|
|
httpsServer.listen(httpsPort, '0.0.0.0', () => {
|
|
console.log(`🔒 HTTPS Server is running on https://localhost:${httpsPort}`)
|
|
if (process.env.SERVER_IP) {
|
|
console.log(`🔒 HTTPS Server is also available on https://${process.env.SERVER_IP}:${httpsPort}`)
|
|
}
|
|
console.log('📱 For Android testing, use the IP address URL')
|
|
})
|
|
|
|
// Optional: Start HTTP server that redirects to HTTPS
|
|
const httpApp = express()
|
|
httpApp.use((req, res) => {
|
|
const host = req.headers.host?.split(':')[0] || 'localhost'
|
|
const redirectUrl = `https://${host}:${httpsPort}${req.url}`
|
|
res.redirect(301, redirectUrl)
|
|
})
|
|
|
|
const httpServer = http.createServer(httpApp)
|
|
httpServer.listen(httpPort, '0.0.0.0', () => {
|
|
console.log(`🔄 HTTP Server (redirect) is running on http://localhost:${httpPort}`)
|
|
})
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGTERM', () => {
|
|
console.log('Shutting down servers...')
|
|
httpsServer.close(() => {
|
|
console.log('HTTPS server closed')
|
|
})
|
|
httpServer.close(() => {
|
|
console.log('HTTP server closed')
|
|
})
|
|
})
|
|
|
|
} catch (error) {
|
|
console.error('❌ Failed to start HTTPS server:', error.message)
|
|
console.log('Falling back to HTTP server...')
|
|
startHttpServer()
|
|
}
|
|
} else {
|
|
startHttpServer()
|
|
}
|
|
|
|
function startHttpServer() {
|
|
const httpServer = http.createServer(app)
|
|
httpServer.listen(httpPort, '0.0.0.0', () => {
|
|
console.log(`🌐 HTTP Server is running on http://localhost:${httpPort}`)
|
|
if (process.env.SERVER_IP) {
|
|
console.log(`🌐 HTTP Server is also available on http://${process.env.SERVER_IP}:${httpPort}`)
|
|
}
|
|
console.log('⚠️ Using HTTP - HTTPS recommended for mobile testing')
|
|
})
|
|
}
|
|
}
|
|
|
|
startServer()
|