Add SSL certificates and database schema for attendance management system
- Added SSL certificate (cert.pem) for secure communication. - Added private key (key.pem) for SSL configuration. - Added root CA certificate (rootCA.pem) for certificate validation. - Created initial database schema in dev.sql with tables for system configuration, worker profiles, attendance rules, notifications, geofence zones, shift schedules, overtime records, leave requests, and payroll records. - Inserted sample data for system configuration and attendance rules.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEajCCAtKgAwIBAgIQCidY0lKaDwojBgr6MpeBzzANBgkqhkiG9w0BAQsFADCB
|
||||
kTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTMwMQYDVQQLDCpNQUlM
|
||||
XG1hc29uZ3lhbkBERVNLVE9QLUlRVThEREQgKG1hc29uZ3lhbikxOjA4BgNVBAMM
|
||||
MW1rY2VydCBNQUlMXG1hc29uZ3lhbkBERVNLVE9QLUlRVThEREQgKG1hc29uZ3lh
|
||||
bikwHhcNMjUwNzA0MDc0NjExWhcNMjcxMDA0MDc0NjExWjBeMScwJQYDVQQKEx5t
|
||||
a2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxMzAxBgNVBAsMKk1BSUxcbWFz
|
||||
b25neWFuQERFU0tUT1AtSVFVOERERCAobWFzb25neWFuKTCCASIwDQYJKoZIhvcN
|
||||
AQEBBQADggEPADCCAQoCggEBANl8SofEGCDGYv2J22Qanu6LgxvvKd9wKB1Lf2x6
|
||||
eBD84tHmVZXKuQElo9ZkEbljKA9M8dNCTrxNFzGL6dB2b3fRHBnEYhiANKnMohgb
|
||||
oul+Tiq2/Pye4SHWglvsM6DboImARRW58L8FyA3mnS9VgS7TUb3W2tRQhLHU1s/R
|
||||
QjZulIQvpe+k0dW+S1zd7wBg790K5GNs9va/8KEM1v3esBNOpCbKeWzeRT/Si9ZA
|
||||
Dfm72SSWslHQEXtuz8AQVtfk0qJMUB0URmyadir0aJwuDC6m5iQSKtLTvQp+n0/Z
|
||||
lundQQbsnm71FnCAD9PSz+IaB3euEOwUGbGnDW9+10kGTekCAwEAAaNwMG4wDgYD
|
||||
VR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFElR
|
||||
m5C15845O14vXvSvwjxwtiJEMCYGA1UdEQQfMB2CCWxvY2FsaG9zdIcECgACAocE
|
||||
fwAAAYcEwKgkNjANBgkqhkiG9w0BAQsFAAOCAYEAsOdvadeTxsAT0Le63PPEYPiZ
|
||||
drkEJdTyu9Thv9nFhLCD4vUYIZrlE3brFXD1iVTR1muJsalfnmW9azIwGBHw52bZ
|
||||
B2XdA6HNZEklSRtqNMEAGJsdnbGuCTPa1lLNuzCQodSnmbvu6Y5K13Pq/asl3DVW
|
||||
h/hczwX5NrQvlvyDwI0kVSDRmEb5AYnEic5h64gEyILTVWopT8RzA+B8AtW3oP3d
|
||||
pfoCErwQvxfkNd3UGWk+rDlQWwApzh+N4P+3vAjhAra7Yoj+JtT0SnXeAjXhbB0E
|
||||
WmDcMNQwxUg1FN5ATR5pAMoSSNviLaf/jYb93naZ6YZKgSfSIKNgUJz+ppgHNBFr
|
||||
326JOYH0yzyhWXUXchzsn1ytMkhddNVZhRbGceOkyZEkaSynZR4om8ZGxPJYfCBB
|
||||
m9sH27eCeJBy9DXk0ZUkJg+y3C+jizenHiPnED92Z1EZ0ke7fNufiVZs0yQl2uxg
|
||||
V5mgoQSLxu4LHXQnTm/NQugY9S8rfbz510WutGKi
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,39 +0,0 @@
|
||||
import mysql from 'mysql2/promise'
|
||||
import bcrypt from 'bcrypt'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
dotenv.config()
|
||||
|
||||
async function hashPasswords() {
|
||||
const db = await mysql.createConnection({
|
||||
host: process.env.DB_HOST,
|
||||
user: process.env.DB_USERNAME,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_DATABASE,
|
||||
port: process.env.DB_PORT,
|
||||
})
|
||||
|
||||
try {
|
||||
const [workers] = await db.execute('SELECT id, password_hash FROM workers')
|
||||
|
||||
for (const worker of workers) {
|
||||
if (worker.password_hash && !worker.password_hash.startsWith('$2b$')) {
|
||||
const saltRounds = 10
|
||||
const hashedPassword = await bcrypt.hash(worker.password_hash, saltRounds)
|
||||
await db.execute('UPDATE workers SET password_hash = ? WHERE id = ?', [
|
||||
hashedPassword,
|
||||
worker.id,
|
||||
])
|
||||
console.log(`Hashed password for worker with ID: ${worker.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Password hashing complete.')
|
||||
} catch (error) {
|
||||
console.error('Error hashing passwords:', error)
|
||||
} finally {
|
||||
await db.end()
|
||||
}
|
||||
}
|
||||
|
||||
hashPasswords()
|
||||
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZfEqHxBggxmL9
|
||||
idtkGp7ui4Mb7ynfcCgdS39sengQ/OLR5lWVyrkBJaPWZBG5YygPTPHTQk68TRcx
|
||||
i+nQdm930RwZxGIYgDSpzKIYG6Lpfk4qtvz8nuEh1oJb7DOg26CJgEUVufC/BcgN
|
||||
5p0vVYEu01G91trUUISx1NbP0UI2bpSEL6XvpNHVvktc3e8AYO/dCuRjbPb2v/Ch
|
||||
DNb93rATTqQmynls3kU/0ovWQA35u9kklrJR0BF7bs/AEFbX5NKiTFAdFEZsmnYq
|
||||
9GicLgwupuYkEirS070Kfp9P2Zbp3UEG7J5u9RZwgA/T0s/iGgd3rhDsFBmxpw1v
|
||||
ftdJBk3pAgMBAAECggEAIeDztzx7ybc9umMcMvbWpTBEZziVXEIbbZzSJ7LYO0U5
|
||||
jBsGYAQpV51mbUI/ZJKmreN9lDwzCbA0mbpC3P9mE9MWPolSAqEOExlWcszzTs4n
|
||||
HQ5OUIfraBsDSZB85mTwGBtMJ7tEXm1nIYs4FySJsCKpDBqJEiPM1+rg35SobNP5
|
||||
aOvuLgXe3V6wVuihakoGj8nUtCgKsPr/14ybcF6Fcv5ULI6Tls0G8HOY92Kesb/o
|
||||
NZL1YmMVevY+RKYzrZKca6mRanMIjnjrnYGX5V404mh6GQKpGdgrcMEONMbJje2H
|
||||
44MjyJYhQ67/ItOKOuC1JG1LuRq/5SXTAS2WW7g+1QKBgQDiWefUn2v3pYd4CIFd
|
||||
Bz43TpHuQZiqX5UOvPFOrk5LT+EhYHTpSCThrc5piqk+XsnV3G1dyDnbBK8k4FPa
|
||||
yyrUuNOSvQlspSr0u++5i7cRLwq7C6kRtTzW8nr6Az8bE6u1prvXKFIWKP/doWeg
|
||||
U7jPMCVKN+oxvNN6Fi0meecLxwKBgQD1+RkfrUCg7xpr+gn2R2LxryL2u/oxVRmo
|
||||
4TZqBQoXcQJBx+UrTcIL8XENohYYI/7HCZfD/cBxpFGNqclD3DjzjH2NZ43MBlbN
|
||||
up3wD+Ks2LVOilyOrxK3be/cnvPyQJantd/NBnHOTsQoBUPdhbrqdyrjYW0o4WZQ
|
||||
5c36f934zwKBgQCRiTEQeviWoG279ewHfpK4SOJ3iOG6Gf7jHQUii9x3fALKzRQe
|
||||
sm5UVMZ1AdzT52prAXGobQcWFarvUPVZpmwBnl0a6kTXAFPgS75VVMn+WHrTzSmF
|
||||
4zwdEIeVnOTEah9riqsYKiqtaOsq+45/fZVEUjaHw+/mzvxCcWPSa2rtHQKBgEUe
|
||||
amDsXmzaw6Hz8TizdqpTfI+44uVZ9IvwPUotgFh1+Rxi/5LbltukTRB3q528/6sO
|
||||
lwcMFzfX5NLaEyRujdJieCV0I/RhE6Nb/WWoERphCxG276topunEitKEGCjK3Yrj
|
||||
ILCMTw6aM6TLVfa5zXx1YCflCLekHww8h1UM+WMhAoGAH6U1XzkW3ozty7sQ5vxZ
|
||||
jzri0xUpp06EA/EtfhkCRPgaYCkL5aXan+jNAZPfTG6mGudULWjTIfEEQrMJ54CN
|
||||
sItMoPP2S4EDuj4xdQWe8eTeMqtGG/lAmG2Yr9QajWofNLwaBtsXANYCDGadNUxa
|
||||
2pog6+BDaFEC64IwkoBYgZ8=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,29 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIE8zCCA1ugAwIBAgIQGdkeqkj233eI/7av8ih4aTANBgkqhkiG9w0BAQsFADCB
|
||||
kTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTMwMQYDVQQLDCpNQUlM
|
||||
XG1hc29uZ3lhbkBERVNLVE9QLUlRVThEREQgKG1hc29uZ3lhbikxOjA4BgNVBAMM
|
||||
MW1rY2VydCBNQUlMXG1hc29uZ3lhbkBERVNLVE9QLUlRVThEREQgKG1hc29uZ3lh
|
||||
bikwHhcNMjUwNzAzMDg1NDI2WhcNMzUwNzAzMDg1NDI2WjCBkTEeMBwGA1UEChMV
|
||||
bWtjZXJ0IGRldmVsb3BtZW50IENBMTMwMQYDVQQLDCpNQUlMXG1hc29uZ3lhbkBE
|
||||
RVNLVE9QLUlRVThEREQgKG1hc29uZ3lhbikxOjA4BgNVBAMMMW1rY2VydCBNQUlM
|
||||
XG1hc29uZ3lhbkBERVNLVE9QLUlRVThEREQgKG1hc29uZ3lhbikwggGiMA0GCSqG
|
||||
SIb3DQEBAQUAA4IBjwAwggGKAoIBgQC5YL+VRL/bDBg0SP78IZTCemeLr7Q4Zxtg
|
||||
8MiaWrDnh6ssVFzmAY3PEnfTdSL/j0JV2I0cSZhmMkUAzoo7136paLA3aGD4QP0B
|
||||
fDEt6xQZF30U3bRhTglEY8a1zhy6fJGTYOcl2/OTbS0q90fEaLx8wkVa0lf/2wA7
|
||||
fYG65BSu9CgTdob6NBWbI3Jpsesxd+36WZCqa6ZPSk07nXozqjMFsG8CThr1Wmei
|
||||
mZJZF6+ji0mI6RqiqgdWrKBp2FZbPERQS+QfYfKD5/N0cWpwUAxejSLlPxU886Ns
|
||||
Tcld9vxHQjzcE0afJe7rO4IrzzIeL1oLsz3xhEBgn8JCUeWbU12pk+9j1z+/M0+U
|
||||
LUt/g+cwHk8fKl7qoL1ydR7afDdFBR8ns+g5l40ZE/uwhgQA8uTsi2E18B5agAtQ
|
||||
C6+dJC4bMiVn9iyCeQmPKS+xw4YOVmn0yfrkqRLRgSZDjQEd4pUAep4J/8WbI1BY
|
||||
lNqRwmqBcLuuyQLpExlMBYPMWiWYBakCAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgIE
|
||||
MBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFElRm5C15845O14vXvSvwjxw
|
||||
tiJEMA0GCSqGSIb3DQEBCwUAA4IBgQAIjI0pKdY1/NKIaCg0WuQcWh8/noTjqYdl
|
||||
7RDJ+JQZB1W0SkN7XvSLiRcEroWalbq9BMwE/bwV9jcgW1NvQ+00VzUWKh0r9z3B
|
||||
xjEnKnK+1pEXRdBfkG6bVi2XehNs7KOvqR07xv7o9GdB41R7TSo1Vr228ot82FNO
|
||||
6B2iDumfIr9RESsx8nVntHvRuFTee/DlhVUEgJPWmw0Kwcewmd2p5XNijdA2V2nI
|
||||
zwhUxQuuu0LtV8RXmBi5vDanrJHwZZ1kFvGG9SGiVNx6aEtGBjTMIFRpQyLFzeq9
|
||||
TPbVLsEGjvi8wLqO8U/aj56BEkFNKAx0idohgyfF2qohRMXoL0MRtEQIpJdL2kMP
|
||||
Gqg1aY7MWooEM9swji1hHuoDwLriVNS6W3LvT9qXWlI3e/J7f5aLT/QyP4VUW+4N
|
||||
1oGUL54aXCMYymVXooU3QomakxCildlGbH0jdcf8uX8JVnI0Zeo3ftCmtf46Q+Lu
|
||||
7Mhu4NO8kQGHH/m0wQwwahh+mBfwYwk=
|
||||
-----END CERTIFICATE-----
|
||||
+599
-80
@@ -1,23 +1,166 @@
|
||||
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'
|
||||
// --- FIX START ---
|
||||
// Import only the required functions from turf
|
||||
import { point, polygon, booleanPointInPolygon, pointToLineDistance } from '@turf/turf'
|
||||
// --- FIX END ---
|
||||
|
||||
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()
|
||||
dotenv.config({ path: path.join(path.dirname(fileURLToPath(import.meta.url)), '.env') })
|
||||
|
||||
|
||||
const app = express()
|
||||
const port = 3000
|
||||
|
||||
// --- Database Connection ---
|
||||
const db = mysql.createPool({
|
||||
@@ -42,29 +185,86 @@ async function startServer() {
|
||||
}
|
||||
|
||||
// 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([
|
||||
// [
|
||||
// [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.35311466293217, 23.161344441258407],
|
||||
[113.28591534444001, 23.161344441258407],
|
||||
[113.28591534444001, 23.091366234233973],
|
||||
[113.35311466293217, 23.091366234233973],
|
||||
[113.35311466293217, 23.161344441258407]
|
||||
]
|
||||
])
|
||||
|
||||
|
||||
app.use(cors())
|
||||
// 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
|
||||
// Auth Endpoint with Device UUID validation
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body
|
||||
const { username, password, deviceUuid } = req.body
|
||||
const [rows] = await db.execute(
|
||||
'SELECT id, role, password_hash FROM workers WHERE username = ?',
|
||||
[username],
|
||||
@@ -73,6 +273,17 @@ async function startServer() {
|
||||
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',
|
||||
})
|
||||
@@ -98,77 +309,99 @@ async function startServer() {
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
|
||||
if (err) {
|
||||
return res.sendStatus(403)
|
||||
return res.status(403).json({ message: 'Forbidden' })
|
||||
}
|
||||
|
||||
req.user = user
|
||||
next()
|
||||
})
|
||||
} else {
|
||||
res.sendStatus(401)
|
||||
res.status(401).json({ message: 'Unauthorized' })
|
||||
}
|
||||
}
|
||||
|
||||
// Worker Clock In/Out Endpoint
|
||||
// Worker Clock In/Out Endpoint - Optimized Version
|
||||
app.post('/api/clock', authenticateJWT, async (req, res) => {
|
||||
try {
|
||||
const { userId, eventType, qrCodeValue, latitude, longitude } = req.body
|
||||
const { userId, eventType, qrCodeValue, latitude, longitude, notes } = req.body;
|
||||
const connection = await db.getConnection(); // Get connection from pool first
|
||||
|
||||
// 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
|
||||
// Calculate the distance from the geofence
|
||||
const distance = pointToLineDistance(userLocation, geofence.geometry.coordinates[0], { units: 'meters' });
|
||||
// Create a descriptive note
|
||||
const notes = `Clock-in outside of the zone: ${distance.toFixed(2)} meters.`;
|
||||
// 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])
|
||||
]);
|
||||
|
||||
// Insert the failed attempt into the database
|
||||
await db.execute(
|
||||
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[userId, 'failed', new Date(), qrCodeValue, latitude, longitude, notes]
|
||||
);
|
||||
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 an error to the user
|
||||
return res.status(403).json({ message: `You are not within the allowed work area.` });
|
||||
// --- MODIFICATION END ---
|
||||
}
|
||||
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]
|
||||
);
|
||||
|
||||
const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [
|
||||
qrCodeValue,
|
||||
])
|
||||
await connection.commit();
|
||||
return res.status(403).json({ message: `You are not within the allowed work area.` });
|
||||
}
|
||||
|
||||
if (qrRows.length === 0) {
|
||||
// This code is not in the database at all.
|
||||
return res.status(400).json({ message: 'Invalid QR Code scanned.' })
|
||||
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.' });
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (qrCodeValue === 'FORCE_CLOCK_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();
|
||||
}
|
||||
|
||||
if (!qrRows[0].is_active) {
|
||||
// This code exists but has been deactivated.
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: 'This QR Code has expired and is no longer active.' })
|
||||
}
|
||||
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) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: `You are already clocked ${eventType === 'clock_in' ? 'in' : 'out'}.` })
|
||||
}
|
||||
const timestamp = new Date()
|
||||
await db.execute(
|
||||
'INSERT INTO clock_records (worker_id, event_type, timestamp, qr_code_id, latitude, longitude) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[userId, eventType, timestamp, qrCodeValue, latitude, longitude],
|
||||
)
|
||||
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.' });
|
||||
}
|
||||
})
|
||||
|
||||
@@ -690,15 +923,301 @@ async function startServer() {
|
||||
}
|
||||
})
|
||||
|
||||
// --- Server Start ---
|
||||
// const httpsOptions = {
|
||||
// key: fs.readFileSync(process.env.SSL_KEY_PATH),
|
||||
// cert: fs.readFileSync(process.env.SSL_CERT_PATH),
|
||||
// }
|
||||
// --- NEW NATIVE FEATURES API ENDPOINTS ---
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on http://localhost:${port}`)
|
||||
// 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 - 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
|
||||
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
# Host: localhost (Version: 5.7.26)
|
||||
# Date: 2025-07-14 12:03:00
|
||||
# Generator: MySQL-Front 5.3 (Build 4.234)
|
||||
|
||||
/*!40101 SET NAMES utf8 */;
|
||||
|
||||
#
|
||||
# Structure for table "system_config"
|
||||
#
|
||||
|
||||
CREATE TABLE `system_config` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`config_key` varchar(255) NOT NULL,
|
||||
`config_value` text,
|
||||
`config_type` enum('string','number','boolean','json') DEFAULT 'string',
|
||||
`description` text,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `config_key` (`config_key`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='System configuration settings';
|
||||
|
||||
#
|
||||
# Data for table "system_config"
|
||||
#
|
||||
|
||||
INSERT INTO `system_config` VALUES
|
||||
(1,'geofence_enabled','true','boolean','Enable geofence checking for clock-in/out','2025-07-14 12:00:00','2025-07-14 12:00:00'),
|
||||
(2,'work_start_time','09:00','string','Standard work start time (HH:MM)','2025-07-14 12:00:00','2025-07-14 12:00:00'),
|
||||
(3,'work_end_time','17:00','string','Standard work end time (HH:MM)','2025-07-14 12:00:00','2025-07-14 12:00:00'),
|
||||
(4,'late_threshold_minutes','15','number','Minutes after start time to consider as late','2025-07-14 12:00:00','2025-07-14 12:00:00'),
|
||||
(5,'early_threshold_minutes','15','number','Minutes before end time to consider as early leave','2025-07-14 12:00:00','2025-07-14 12:00:00'),
|
||||
(6,'qr_codes_enabled','true','boolean','Enable QR code scanning for clock-in/out','2025-07-14 12:00:00','2025-07-14 12:00:00'),
|
||||
(7,'location_tracking_enabled','true','boolean','Enable GPS location tracking','2025-07-14 12:00:00','2025-07-14 12:00:00'),
|
||||
(8,'security_check_enabled','true','boolean','Enable device security checks','2025-07-14 12:00:00','2025-07-14 12:00:00');
|
||||
|
||||
#
|
||||
# Structure for table "worker_profiles"
|
||||
#
|
||||
|
||||
CREATE TABLE `worker_profiles` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`worker_id` int(11) NOT NULL,
|
||||
`department` varchar(255) DEFAULT NULL,
|
||||
`position` varchar(255) DEFAULT NULL,
|
||||
`phone` varchar(50) DEFAULT NULL,
|
||||
`email` varchar(255) DEFAULT NULL,
|
||||
`emergency_contact` varchar(255) DEFAULT NULL,
|
||||
`emergency_phone` varchar(50) DEFAULT NULL,
|
||||
`address` text,
|
||||
`hire_date` date DEFAULT NULL,
|
||||
`salary_type` enum('hourly','monthly','daily') DEFAULT 'hourly',
|
||||
`hourly_rate` decimal(10,2) DEFAULT NULL,
|
||||
`monthly_salary` decimal(10,2) DEFAULT NULL,
|
||||
`bank_account` varchar(50) DEFAULT NULL,
|
||||
`tax_id` varchar(50) DEFAULT NULL,
|
||||
`social_security_id` varchar(50) DEFAULT NULL,
|
||||
`notes` text,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `worker_id` (`worker_id`),
|
||||
KEY `idx_department` (`department`),
|
||||
KEY `idx_position` (`position`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Extended worker profile information';
|
||||
|
||||
#
|
||||
# Structure for table "attendance_rules"
|
||||
#
|
||||
|
||||
CREATE TABLE `attendance_rules` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`rule_name` varchar(255) NOT NULL,
|
||||
`rule_type` enum('late','early','absence','overtime') NOT NULL,
|
||||
`condition_type` enum('time','duration','frequency') NOT NULL,
|
||||
`condition_value` varchar(255) NOT NULL,
|
||||
`action_type` enum('warning','deduction','notification') NOT NULL,
|
||||
`action_value` varchar(255) DEFAULT NULL,
|
||||
`is_active` tinyint(1) DEFAULT 1,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_rule_type` (`rule_type`),
|
||||
KEY `idx_is_active` (`is_active`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Attendance rules and policies';
|
||||
|
||||
#
|
||||
# Data for table "attendance_rules"
|
||||
#
|
||||
|
||||
INSERT INTO `attendance_rules` VALUES
|
||||
(1,'Late Arrival','late','time','09:15','warning',NULL,1,'2025-07-14 12:00:00','2025-07-14 12:00:00'),
|
||||
(2,'Early Departure','early','time','16:45','warning',NULL,1,'2025-07-14 12:00:00','2025-07-14 12:00:00'),
|
||||
(3,'Excessive Late','late','time','09:30','deduction','30',1,'2025-07-14 12:00:00','2025-07-14 12:00:00'),
|
||||
(4,'Absent Without Notice','absence','frequency','1','deduction','100',1,'2025-07-14 12:00:00','2025-07-14 12:00:00');
|
||||
|
||||
#
|
||||
# Structure for table "notifications"
|
||||
#
|
||||
|
||||
CREATE TABLE `notifications` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`user_id` int(11) DEFAULT NULL,
|
||||
`type` enum('system','alert','reminder','report') NOT NULL,
|
||||
`title` varchar(255) NOT NULL,
|
||||
`message` text,
|
||||
`data` json DEFAULT NULL,
|
||||
`is_read` tinyint(1) DEFAULT 0,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_type` (`type`),
|
||||
KEY `idx_is_read` (`is_read`),
|
||||
KEY `idx_created_at` (`created_at`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='System notifications for users';
|
||||
|
||||
#
|
||||
# Structure for table "geofence_zones"
|
||||
#
|
||||
|
||||
CREATE TABLE `geofence_zones` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL,
|
||||
`description` text,
|
||||
`coordinates` json NOT NULL,
|
||||
`center_lat` decimal(10,8) NOT NULL,
|
||||
`center_lng` decimal(11,8) NOT NULL,
|
||||
`radius_meters` int(11) DEFAULT 0,
|
||||
`is_active` tinyint(1) DEFAULT 1,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_is_active` (`is_active`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Geofence zones for location-based attendance';
|
||||
|
||||
#
|
||||
# Data for table "geofence_zones"
|
||||
#
|
||||
|
||||
INSERT INTO `geofence_zones` VALUES
|
||||
(1,'Main Factory','Main factory area','[[113.35311466293217,23.161344441258407],[113.28591534444001,23.161344441258407],[113.28591534444001,23.091366234233973],[113.35311466293217,23.091366234233973],[113.35311466293217,23.161344441258407]]',23.126355,113.319515,5000,1,'2025-07-14 12:00:00','2025-07-14 12:00:00');
|
||||
|
||||
#
|
||||
# Structure for table "shift_schedules"
|
||||
#
|
||||
|
||||
CREATE TABLE `shift_schedules` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`worker_id` int(11) NOT NULL,
|
||||
`shift_date` date NOT NULL,
|
||||
`shift_type` enum('morning','afternoon','night','custom') NOT NULL,
|
||||
`start_time` time NOT NULL,
|
||||
`end_time` time NOT NULL,
|
||||
`break_duration` int(11) DEFAULT 60,
|
||||
`notes` text,
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_worker_id` (`worker_id`),
|
||||
KEY `idx_shift_date` (`shift_date`),
|
||||
KEY `idx_shift_type` (`shift_type`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Worker shift schedules';
|
||||
|
||||
#
|
||||
# Structure for table "overtime_records"
|
||||
#
|
||||
|
||||
CREATE TABLE `overtime_records` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`worker_id` int(11) NOT NULL,
|
||||
`date` date NOT NULL,
|
||||
`start_time` time NOT NULL,
|
||||
`end_time` time NOT NULL,
|
||||
`duration_minutes` int(11) NOT NULL,
|
||||
`reason` text,
|
||||
`approved_by` int(11) DEFAULT NULL,
|
||||
`status` enum('pending','approved','rejected') DEFAULT 'pending',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_worker_id` (`worker_id`),
|
||||
KEY `idx_date` (`date`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Overtime records and approvals';
|
||||
|
||||
#
|
||||
# Structure for table "leave_requests"
|
||||
#
|
||||
|
||||
CREATE TABLE `leave_requests` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`worker_id` int(11) NOT NULL,
|
||||
`leave_type` enum('sick','vacation','personal','emergency') NOT NULL,
|
||||
`start_date` date NOT NULL,
|
||||
`end_date` date NOT NULL,
|
||||
`duration_days` int(11) NOT NULL,
|
||||
`reason` text,
|
||||
`status` enum('pending','approved','rejected') DEFAULT 'pending',
|
||||
`approved_by` int(11) DEFAULT NULL,
|
||||
`approved_at` timestamp NULL 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 `idx_worker_id` (`worker_id`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_dates` (`start_date`,`end_date`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Leave requests and approvals';
|
||||
|
||||
#
|
||||
# Structure for table "payroll_records"
|
||||
#
|
||||
|
||||
CREATE TABLE `payroll_records` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`worker_id` int(11) NOT NULL,
|
||||
`period_start` date NOT NULL,
|
||||
`period_end` date NOT NULL,
|
||||
`base_salary` decimal(10,2) NOT NULL,
|
||||
`overtime_pay` decimal(10,2) DEFAULT 0,
|
||||
`deductions` decimal(10,2) DEFAULT 0,
|
||||
`net_pay` decimal(10,2) NOT NULL,
|
||||
`status` enum('draft','calculated','approved','paid') DEFAULT 'draft',
|
||||
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_worker_id` (`worker_id`),
|
||||
KEY `idx_period` (`period_start`,`period_end`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Payroll records for workers';
|
||||
Reference in New Issue
Block a user