diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..704fc45 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,107 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +NiLai-Clock is a worker attendance management system with role-based dashboards for workers and managers. It's built as a Vue.js SPA with an Express.js backend, featuring QR code attendance tracking, geofencing, and comprehensive reporting capabilities. + +## Commands + +### Development +- `npm run dev` - Start frontend development server (Vite on port 5173) +- `npm run backend` - Start backend server (Express on port 3000) +- `npm run dev:all` - Run both frontend and backend concurrently +- `npm install` - Install dependencies + +### Production +- `npm run build` - Build for production using Vite +- `npm run preview` - Preview production build locally + +### Code Quality +- `npm run lint` - Run ESLint with auto-fix +- `npm run format` - Format code using Prettier + +## Architecture + +### Frontend (Vue.js) +- **Framework**: Vue 3 with Composition API +- **Build Tool**: Vite with Vue plugin and TailwindCSS +- **Routing**: Vue Router with hash-based routing and role-based guards +- **State**: Session storage for authentication (userId, userRole, token) +- **Styling**: TailwindCSS v4 with responsive design +- **Internationalization**: Vue I18n with English (en) and Malay (ms) locales + +### Backend (Express.js) +- **Server**: Express with CORS, supports both HTTP and HTTPS +- **Database**: MySQL with connection pooling (mysql2) +- **Authentication**: JWT tokens with bcrypt for password hashing +- **Routes**: Separated into manager (`/api/managers/*`) and worker (`/api/*`) routes + +### Key Components Structure + +#### Views (Role-Based) +- **Worker**: `WorkerDashboard`, `WorkerHistory`, `WorkerSettings`, `WorkerChangePassword` +- **Manager**: `ManagerDashboard`, `ManagerAttendanceRecord` +- **Auth**: `Login` (supports both roles) + +#### Components +- **Management**: `PersonnelManagement`, `GeofenceManagement`, `QrCodeManagement`, `KillSwitchManagement` +- **UI**: `Toast` notifications with `useToast` composable +- **Reporting**: `WarningReporting` with CSV export capabilities + +### Authentication Flow +- Role-based routing with `meta: { requiresAuth: true, role: 'worker|manager' }` +- Navigation guards redirect users to appropriate dashboards +- Session storage manages authentication state +- API requests include JWT tokens via Authorization header + +### Key Libraries +- **Maps**: Leaflet with drawing capabilities for geofencing +- **QR Codes**: html5-qrcode for scanning, qrcode for generation +- **Geospatial**: @turf/turf for geographical calculations +- **Data Export**: json2csv for attendance reports +- **UUID**: For device identification and worker login tracking + +## Environment Configuration + +Create `.env` file with: +``` +DB_HOST=your_database_host +DB_USER=your_database_user +DB_PASSWORD=your_database_password +DB_NAME=your_database_name +DB_PORT=your_database_port +VITE_API_BASE_URL=your_api_base_url +``` + +Optional SSL configuration: +- `SSL_ENABLED=true` +- `HTTP_PORT=3000` +- `HTTPS_PORT=3443` + +## Development Notes + +- Frontend uses hash-based routing for better deployment compatibility +- Backend supports both HTTP and HTTPS with configurable ports +- CORS configured for multiple origins including mobile app protocols +- Database connection is tested on server startup with proper error handling +- All API calls go through centralized `apiFetch` utility with error handling +- ESLint configured to ignore unused variables/parameters prefixed with underscore +- Prettier integrated with Vue ESLint config for consistent formatting + +## Database Schema + +The application manages worker attendance with these core entities: +- **Workers**: Authentication, device UUID tracking, role assignment +- **Managers**: Administrative access and permissions +- **Attendance Records**: Clock in/out with location and timestamp data +- **Geofences**: Location boundaries for valid attendance tracking +- **QR Codes**: Dynamic codes for attendance verification + +## Docker Support + +Includes Docker configuration: +- `Dockerfile` - Node.js Alpine-based container +- `docker-compose.yml` - Multi-service setup with nginx reverse proxy +- Production deployment on port 18080 (nginx) and 18081 (app) \ No newline at end of file diff --git a/backend/cert.pem b/backend/cert.pem new file mode 100644 index 0000000..b3ebdd9 --- /dev/null +++ b/backend/cert.pem @@ -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----- diff --git a/backend/key.pem b/backend/key.pem new file mode 100644 index 0000000..f667e5f --- /dev/null +++ b/backend/key.pem @@ -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----- diff --git a/backend/managerRoutes.js b/backend/managerRoutes.js index bd331be..9a91c94 100644 --- a/backend/managerRoutes.js +++ b/backend/managerRoutes.js @@ -16,17 +16,39 @@ export default function(db) { if (err || user.role !== 'manager') { return res.status(403).json({ message: 'Forbidden' }); } - req.user = user; + req.user = { ...user, id: user.userId }; // Correctly map userId to id next(); }); } else { res.status(401).json({ message: 'Unauthorized' }); } }; + + // Middleware to check for specific permissions + const checkPermission = (requiredPermission) => { + return async (req, res, next) => { + try { + const managerId = req.user.id; + const [rows] = await db.execute( + 'SELECT * FROM manager_permissions WHERE manager_id = ?', + [managerId] + ); + + if (rows.length === 0 || !rows[0][requiredPermission]) { + return res.status(403).json({ message: 'Forbidden: Insufficient permissions.' }); + } + next(); + } catch (error) { + console.error('Permission check error:', error); + res.status(500).json({ message: 'Database error during permission check.' }); + } + }; + }; + router.use(authenticateJWT); // --- START: Date Management Routes --- - router.get('/enabled-dates', async (req, res) => { + router.get('/enabled-dates', checkPermission('view_all'), async (req, res) => { try { const [rows] = await db.execute('SELECT YEAR(enabled_date) as year, MONTH(enabled_date) as month, DAY(enabled_date) as day FROM enabled_dates'); // Format date safely using components from the database to avoid timezone shifts @@ -39,7 +61,7 @@ export default function(db) { }); // Definitive version using a dedicated database connection - router.post('/enabled-dates/update', async (req, res) => { + router.post('/enabled-dates/update', checkPermission('manage_resources'), async (req, res) => { let connection; // Define connection here to ensure it's accessible in the 'finally' block try { const { datesToEnable, datesToDisable } = req.body; @@ -77,7 +99,7 @@ export default function(db) { // --- ATTENDANCE & REPORTING --- - router.get('/failed-records', async (req, res) => { + router.get('/failed-records', checkPermission('view_all'), async (req, res) => { try { const { search = '', startDate, endDate } = req.query; if (!startDate || !endDate) { @@ -112,7 +134,7 @@ export default function(db) { } }); - router.get('/failed-records/details', async (req, res) => { + router.get('/failed-records/details', checkPermission('view_all'), async (req, res) => { try { const { workerId, startDate, endDate } = req.query; if (!workerId || !startDate || !endDate) { @@ -139,7 +161,7 @@ export default function(db) { }); // GET attendance records with a modified query to avoid the MySQL 5.7 bug - router.get('/attendance-records/export-raw', async (req, res) => { + router.get('/attendance-records/export-raw', checkPermission('view_all'), async (req, res) => { try { const { workerIds, startDate, endDate } = req.query; if (!startDate || !endDate) { @@ -177,7 +199,7 @@ export default function(db) { } }); - router.post('/add-record', authenticateJWT, async (req, res) => { + router.post('/add-record', checkPermission('edit_workers'), async (req, res) => { try { const { workerId, eventType, timestamp, notes } = req.body @@ -212,7 +234,7 @@ export default function(db) { } }) - router.get('/attendance-records/export', async (req, res) => { + router.get('/attendance-records/export', checkPermission('view_all'), async (req, res) => { try { const { workerIds, startDate, endDate } = req.query; if (!startDate || !endDate) { @@ -295,7 +317,7 @@ export default function(db) { } }); - router.get('/attendance-records', async (req, res) => { + router.get('/attendance-records', checkPermission('view_all'), async (req, res) => { try { const { workerIds, startDate, endDate, format } = req.query; if (!workerIds) { @@ -351,22 +373,107 @@ export default function(db) { // --- All other manager routes remain the same --- + // GET a specific manager's permissions + router.get('/permissions/:id', async (req, res) => { + try { + const requesterId = req.user.id; + const targetId = parseInt(req.params.id, 10); + + // Check if the user is trying to access their own permissions + if (requesterId !== targetId) { + // If not, check if they have permission to manage permissions + const [permissionRows] = await db.execute( + 'SELECT can_manage_permissions FROM manager_permissions WHERE manager_id = ?', + [requesterId] + ); + + if (permissionRows.length === 0 || !permissionRows[0].can_manage_permissions) { + return res.status(403).json({ message: 'Forbidden: Insufficient permissions to view others\' permissions.' }); + } + } + + // If they are accessing their own, or have permission, fetch the target's permissions + const [rows] = await db.execute( + 'SELECT * FROM manager_permissions WHERE manager_id = ?', + [targetId] + ); + + if (rows.length === 0) { + // If no permissions are set, return a default set of all false + const [fields] = await db.execute('DESCRIBE manager_permissions'); + const defaultPermissions = fields.reduce((acc, field) => { + if (field.Field !== 'manager_id') { + acc[field.Field] = 0; // Use 0 for false + } + return acc; + }, {}); + return res.json(defaultPermissions); + } + + // Convert buffer values to booleans + const permissions = Object.entries(rows[0]).reduce((acc, [key, value]) => { + if (key !== 'manager_id') { + acc[key] = Boolean(value); + } + return acc; + }, {}); + + res.json(permissions); + } catch (error) { + console.error('Get manager permissions error:', error); + res.status(500).json({ message: 'Database error fetching manager permissions.', details: error.message }); + } + }); + + // PUT (update) a manager's permissions + router.put('/permissions/:id', checkPermission('manager_permissions'), async (req, res) => { + try { + const { id } = req.params; + const permissions = req.body; + + const fields = [ + 'view_all', 'edit_workers', 'manage_resources', 'manager_permissions' + ]; + const values = fields.map(field => permissions[field] || false); + + // Convert to new simplified permissions schema + const query = ` + INSERT INTO manager_permissions (manager_id, view_all, edit_workers, manage_resources, manager_permissions) + VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + view_all = VALUES(view_all), + edit_workers = VALUES(edit_workers), + manage_resources = VALUES(manage_resources), + manager_permissions = VALUES(manager_permissions) + `; + + const queryParams = [id, ...values]; + + await db.execute(query, queryParams); + + res.status(200).json({ message: 'Permissions updated successfully.' }); + } catch (error) { + console.error('Update manager permissions error:', error); + res.status(500).json({ message: 'Database error updating manager permissions.', details: error.message }); + } + }); + // GET all workers with filtering and pagination - router.get('/workers', async (req, res) => { + router.get('/workers', checkPermission('view_all'), async (req, res) => { try { const { search = '', page = 1, limit = 20 } = req.query; const offset = (parseInt(page) - 1) * parseInt(limit); const searchTerm = `%${search}%`; let baseQuery = ` - SELECT w.id, w.username, w.full_name, w.department, w.position, w.created_at + SELECT w.id, w.username, w.full_name, w.department, w.position, w.created_at, w.status FROM workers w `; let countQuery = `SELECT COUNT(w.id) as totalCount FROM workers w`; const params = []; const countParams = []; - let whereClauses = ["w.role = 'worker'"]; + let whereClauses = ["w.role = 'worker'", "w.status != 'deleted'"]; // Filter out soft-deleted workers if (search) { whereClauses.push(`(w.full_name LIKE ? OR w.department LIKE ?)`); @@ -393,8 +500,91 @@ export default function(db) { } }); + // GET all managers with their permissions + router.get('/managers', checkPermission('manager_permissions'), async (req, res) => { + try { + const { search = '', page = 1, limit = 20 } = req.query; + const offset = (parseInt(page) - 1) * parseInt(limit); + const searchTerm = `%${search}%`; + + let baseQuery = ` + SELECT + w.id, w.username, w.full_name, w.department, w.position, w.created_at, w.status, + mp.* + FROM workers w + LEFT JOIN manager_permissions mp ON w.id = mp.manager_id + `; + let countQuery = `SELECT COUNT(w.id) as totalCount FROM workers w`; + + const params = []; + const countParams = []; + let whereClauses = ["w.role = 'manager'", "w.status != 'deleted'"]; + + if (search) { + whereClauses.push(`(w.full_name LIKE ? OR w.department LIKE ?)`); + params.push(searchTerm, searchTerm); + countParams.push(searchTerm, searchTerm); + } + + if (whereClauses.length > 0) { + const whereString = ` WHERE ${whereClauses.join(' AND ')}`; + baseQuery += whereString; + countQuery += whereString; + } + + baseQuery += ` ORDER BY w.created_at DESC LIMIT ? OFFSET ?`; + params.push(parseInt(limit), offset); + + const [managers] = await db.execute(baseQuery, params); + const [[{ totalCount }]] = await db.execute(countQuery, countParams); + + res.json({ managers, totalCount }); + } catch (error) { + console.error('Get managers error:', error); + res.status(500).json({ message: 'Database error fetching managers.', details: error.message }); + } + }); + + // POST (add) a new manager + router.post('/managers', checkPermission('manager_permissions'), async (req, res) => { + try { + const { username, password, fullName, department, position } = req.body; + if (!username || !password || !fullName) { + return res.status(400).json({ message: 'Username, password, and full name are required.' }); + } + const hashedPassword = await bcrypt.hash(password, 10); + const [result] = await db.execute( + 'INSERT INTO workers (username, password_hash, full_name, role, department, position, status) VALUES (?, ?, ?, ?, ?, ?, ?)', + [username, hashedPassword, fullName, 'manager', department, position, 'active'] + ); + + // Set default view_all permission + await db.execute( + 'INSERT INTO manager_permissions (manager_id, view_all) VALUES (?, ?)', + [result.insertId, true] + ); + + res.status(201).json({ + id: result.insertId, + username, + fullName, + role: 'manager', + department, + position, + status: 'active', + view_all: true + }); + } catch (error) { + console.error('Add manager error:', error); + if (error.code === 'ER_DUP_ENTRY') { + return res.status(409).json({ message: 'Username already exists.' }); + } + res.status(500).json({ message: 'Database error adding manager.', details: error.message }); + } + }); + // POST (add) a new worker - router.post('/workers', async (req, res) => { + router.post('/workers', checkPermission('edit_workers'), async (req, res) => { try { const { username, password, fullName, department, position, role = 'worker' } = req.body; if (!username || !password || !fullName) { @@ -402,10 +592,10 @@ export default function(db) { } const hashedPassword = await bcrypt.hash(password, 10); const [result] = await db.execute( - 'INSERT INTO workers (username, password_hash, full_name, role, department, position) VALUES (?, ?, ?, ?, ?, ?)', - [username, hashedPassword, fullName, role, department, position] + 'INSERT INTO workers (username, password_hash, full_name, role, department, position, status) VALUES (?, ?, ?, ?, ?, ?, ?)', + [username, hashedPassword, fullName, role, department, position, 'active'] // Default status to 'active' ); - res.status(201).json({ id: result.insertId, username, fullName, role, department, position }); + res.status(201).json({ id: result.insertId, username, fullName, role, department, position, status: 'active' }); } catch (error) { console.error('Add worker error:', error); if (error.code === 'ER_DUP_ENTRY') { @@ -415,23 +605,132 @@ export default function(db) { } }); - // DELETE a worker - router.delete('/workers/:id', async (req, res) => { + // Soft DELETE a worker (update status to 'deleted') + router.delete('/workers/:id', checkPermission('edit_workers'), async (req, res) => { try { const { id } = req.params; - const [result] = await db.execute("DELETE FROM workers WHERE id = ? AND role = 'worker'", [id]); + const [result] = await db.execute("UPDATE workers SET status = 'deleted' WHERE id = ? AND role = 'worker'", [id]); if (result.affectedRows === 0) { - return res.status(404).json({ message: 'Worker not found.' }); + return res.status(404).json({ message: 'Worker not found or already deleted.' }); + } + res.status(204).send(); // Maintain existing response for client compatibility + } catch (error) { + console.error('Soft delete worker error:', error); + res.status(500).json({ message: 'Database error soft deleting worker.', details: error.message }); + } + }); + + // Soft DELETE a manager (update status to 'deleted') + router.delete('/managers/:id', checkPermission('manager_permissions'), async (req, res) => { + try { + const { id } = req.params; + const [result] = await db.execute("UPDATE workers SET status = 'deleted' WHERE id = ? AND role = 'manager'", [id]); + if (result.affectedRows === 0) { + return res.status(404).json({ message: 'Manager not found or already deleted.' }); } res.status(204).send(); } catch (error) { - console.error('Delete worker error:', error); - res.status(500).json({ message: 'Database error deleting worker.', details: error.message }); + console.error('Soft delete manager error:', error); + res.status(500).json({ message: 'Database error soft deleting manager.', details: error.message }); + } + }); + + // PUT (update) a worker's details (department, position, status) + router.put('/workers/:id', checkPermission('edit_workers'), async (req, res) => { + try { + const { id } = req.params; + const { department, position, status } = req.body; + + // Basic validation + if (!department && !position && !status) { + return res.status(400).json({ message: 'No update information provided.' }); + } + if (status && !['active', 'inactive'].includes(status)) { + return res.status(400).json({ message: 'Invalid status value.' }); + } + + let updateQuery = 'UPDATE workers SET'; + const params = []; + const fieldsToUpdate = []; + + if (department) { + fieldsToUpdate.push('department = ?'); + params.push(department); + } + if (position) { + fieldsToUpdate.push('position = ?'); + params.push(position); + } + if (status) { + fieldsToUpdate.push('status = ?'); + params.push(status); + } + + updateQuery += ` ${fieldsToUpdate.join(', ')} WHERE id = ? AND role = 'worker'`; + params.push(id); + + const [result] = await db.execute(updateQuery, params); + + if (result.affectedRows === 0) { + return res.status(404).json({ message: 'Worker not found.' }); + } + + res.status(200).json({ message: 'Worker details updated successfully.' }); + } catch (error) { + console.error('Update worker details error:', error); + res.status(500).json({ message: 'Database error updating worker details.', details: error.message }); + } + }); + + // PUT (update) a manager's details (department, position, status) + router.put('/managers/:id', checkPermission('manager_permissions'), async (req, res) => { + try { + const { id } = req.params; + const { department, position, status } = req.body; + + // Basic validation + if (!department && !position && !status) { + return res.status(400).json({ message: 'No update information provided.' }); + } + if (status && !['active', 'inactive'].includes(status)) { + return res.status(400).json({ message: 'Invalid status value.' }); + } + + let updateQuery = 'UPDATE workers SET'; + const params = []; + const fieldsToUpdate = []; + + if (department) { + fieldsToUpdate.push('department = ?'); + params.push(department); + } + if (position) { + fieldsToUpdate.push('position = ?'); + params.push(position); + } + if (status) { + fieldsToUpdate.push('status = ?'); + params.push(status); + } + + updateQuery += ` ${fieldsToUpdate.join(', ')} WHERE id = ? AND role = 'manager'`; + params.push(id); + + const [result] = await db.execute(updateQuery, params); + + if (result.affectedRows === 0) { + return res.status(404).json({ message: 'Manager not found.' }); + } + + res.status(200).json({ message: 'Manager details updated successfully.' }); + } catch (error) { + console.error('Update manager details error:', error); + res.status(500).json({ message: 'Database error updating manager details.', details: error.message }); } }); // PUT (update) a worker's password - router.put('/workers/:workerId/password', async (req, res) => { + router.put('/workers/:workerId/password', checkPermission('edit_workers'), async (req, res) => { try { const { workerId } = req.params; const { newPassword } = req.body; @@ -450,24 +749,57 @@ export default function(db) { } }); - // PUT (clear) a worker's device UUID - router.put('/workers/:workerId/reset-device', async (req, res) => { + // PUT (update) a manager's password + router.put('/managers/:managerId/password', checkPermission('manager_permissions'), async (req, res) => { + try { + const { managerId } = 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 hashedPassword = await bcrypt.hash(newPassword, 10); + const [result] = await db.execute("UPDATE workers SET password_hash = ? WHERE id = ? AND role = 'manager'", [hashedPassword, managerId]); + if (result.affectedRows === 0) { + return res.status(404).json({ message: 'Manager not found.' }); + } + res.status(200).json({ message: 'Password updated successfully.' }); + } catch (error) { + console.error('Update manager password error:', error); + res.status(500).json({ message: 'Database error updating manager password.', details: error.message }); + } + }); + + // PUT (clear) a worker's device UUID and/or update status + router.put('/workers/:workerId/reset-device', checkPermission('edit_workers'), async (req, res) => { try { const { workerId } = req.params; - const [result] = await db.execute("UPDATE workers SET device_uuid = NULL WHERE id = ?", [workerId]); + const { status } = req.body; // Optional status field + + let updateQuery = "UPDATE workers SET device_uuid = NULL"; + const params = [workerId]; + + if (status && ['active', 'inactive', 'deleted'].includes(status)) { + updateQuery += ", status = ?"; + params.unshift(status); // Add status to the beginning of params for correct order + } + + updateQuery += " WHERE id = ?"; + + const [result] = await db.execute(updateQuery, params); + if (result.affectedRows === 0) { return res.status(404).json({ message: 'Worker not found.' }); } - res.status(200).json({ message: 'Device registration cleared.' }); + res.status(200).json({ message: 'Device registration cleared and/or status updated.' }); } catch (error) { - console.error('Reset device error:', error); - res.status(500).json({ message: 'Database error resetting device.', details: error.message }); + console.error('Reset device/update status error:', error); + res.status(500).json({ message: 'Database error resetting device or updating status.', details: error.message }); } }); // Geofence Management Routes - router.get('/geofences', async (req, res) => { + router.get('/geofences', checkPermission('view_all'), async (req, res) => { try { const [rows] = await db.execute( 'SELECT id, name, coordinates, is_active, created_at FROM geofences ORDER BY created_at DESC' @@ -483,7 +815,7 @@ export default function(db) { } }); - router.post('/geofences', async (req, res) => { + router.post('/geofences', checkPermission('manage_resources'), async (req, res) => { try { const { name, coordinates } = req.body; if (!name || !coordinates) { @@ -508,7 +840,7 @@ export default function(db) { } }); - router.put('/geofences/:id', async (req, res) => { + router.put('/geofences/:id', checkPermission('manage_resources'), async (req, res) => { try { const { id } = req.params; const { is_active } = req.body; @@ -533,7 +865,7 @@ export default function(db) { } }); - router.delete('/geofences/:id', async (req, res) => { + router.delete('/geofences/:id', checkPermission('manage_resources'), async (req, res) => { try { const { id } = req.params; const [result] = await db.execute('DELETE FROM geofences WHERE id = ?', [id]); @@ -551,7 +883,7 @@ export default function(db) { // QR Code Management Routes - router.get('/qr-codes', authenticateJWT, async (req, res) => { + router.get('/qr-codes', checkPermission('view_all'), async (req, res) => { try { const [rows] = await db.execute( 'SELECT id, name, is_active, created_at FROM qr_codes ORDER BY created_at DESC' @@ -563,7 +895,7 @@ export default function(db) { } }); - router.post('/qr-codes', authenticateJWT, async (req, res) => { + router.post('/qr-codes', checkPermission('manage_resources'), async (req, res) => { try { const { name } = req.body; if (!name) return res.status(400).json({ message: 'QR Code name is required.' }); @@ -586,7 +918,7 @@ export default function(db) { } }); - router.put('/qr-codes/:id', authenticateJWT, async (req, res) => { + router.put('/qr-codes/:id', checkPermission('manage_resources'), async (req, res) => { try { const { id } = req.params; // Handle both isActive (camelCase) and is_active (snake_case) @@ -612,7 +944,7 @@ export default function(db) { } }); - router.delete('/qr-codes/:id', authenticateJWT, async (req, res) => { + router.delete('/qr-codes/:id', checkPermission('manage_resources'), async (req, res) => { try { const { id } = req.params; const [result] = await db.execute( diff --git a/backend/rootCA.pem b/backend/rootCA.pem new file mode 100644 index 0000000..f8213ef --- /dev/null +++ b/backend/rootCA.pem @@ -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----- diff --git a/backend/workerRoutes.js b/backend/workerRoutes.js index f64320f..9eb3239 100644 --- a/backend/workerRoutes.js +++ b/backend/workerRoutes.js @@ -24,23 +24,55 @@ async function isClockingEnabled(db) { export default function(db) { const router = express.Router(); + // Set DEVICE_UUID_ENABLED to false to completely disable device UUID checking + const DEVICE_UUID_ENABLED = false; + const REQUIRE_DEVICE_FOR_WORKERS = true; + const AUTO_REGISTER_NEW_DEVICES = true; + router.post('/auth/login', async (req, res) => { const { username, password, deviceUuid } = req.body; - const [rows] = await db.execute('SELECT id, role, password_hash FROM workers WHERE username = ?', [username]); + const [rows] = await db.execute('SELECT id, role, password_hash, status FROM workers WHERE username = ?', [username]); if (rows.length === 0) { return res.status(401).json({ message: 'Invalid credentials' }); } const user = rows[0]; + + // Check if the user's status is 'active' + if (user.status !== 'active') { + return res.status(401).json({ message: 'Invalid credentials' }); + } + const passwordMatch = await bcrypt.compare(password, user.password_hash); if (!passwordMatch) { return res.status(401).json({ message: 'Invalid credentials' }); } - if (deviceUuid && user.role !== 'manager') { - const deviceValidation = await validateDeviceForUser(user.id, deviceUuid, db); - if (!deviceValidation.valid) { - return res.status(403).json({ message: deviceValidation.message }); + + // Device UUID handling - controlled by configuration flags above + if (DEVICE_UUID_ENABLED && user.role === 'worker') { + const [deviceRows] = await db.execute('SELECT device_uuid FROM workers WHERE id = ?', [user.id]); + const existingDeviceUuid = deviceRows[0].device_uuid; + + if (existingDeviceUuid) { + if (deviceUuid && deviceUuid !== existingDeviceUuid) { + return res.status(403).json({ message: 'deviceMismatch' }); + } else if (!deviceUuid) { + return res.status(403).json({ message: 'useMobileApp' }); + } + } else { + // User has no registered device + if (deviceUuid && AUTO_REGISTER_NEW_DEVICES) { + const deviceResult = await validateDeviceForUser(user.id, deviceUuid, db); + if (!deviceResult.valid) { + return res.status(500).json({ message: 'deviceRegistrationFailed' }); + } + // console.log(`Device UUID registered for worker ${user.id}: ${deviceUuid}`); + } else if (!deviceUuid && REQUIRE_DEVICE_FOR_WORKERS) { + return res.status(403).json({ message: 'deviceRequired' }); + } } } + + // Managers can always login, workers without device_uuid can login const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h' }); res.json({ token }); }); @@ -53,7 +85,7 @@ export default function(db) { if (err) { return res.status(403).json({ message: 'Invalid or expired token' }); } - req.user = user; + req.user = { ...user, id: user.userId }; // Correctly map userId to id next(); }); } else { diff --git a/dev.sql b/dev.sql deleted file mode 100644 index 05787a8..0000000 --- a/dev.sql +++ /dev/null @@ -1,207 +0,0 @@ -# Host: localhost (Version: 5.7.26) -# Date: 2025-07-16 13:39:50 -# Generator: MySQL-Front 5.3 (Build 4.234) - -/*!40101 SET NAMES utf8 */; - -# -# Structure for table "app_blacklist" -# - -CREATE TABLE `app_blacklist` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `package_name` varchar(255) NOT NULL, - `reason` varchar(255) DEFAULT NULL, - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `package_name` (`package_name`) -) ENGINE=MyISAM AUTO_INCREMENT=50 DEFAULT CHARSET=utf8; - -# -# Data for table "app_blacklist" -# - -INSERT INTO `app_blacklist` VALUES (4,'com.lexa.fakegps','GPS Spoofing App','2025-07-09 11:59:55'),(5,'com.incorporateapps.fakegps.fre','GPS Spoofing App','2025-07-09 11:59:55'),(6,'com.blogspot.newapphorizons.fakegps','GPS Spoofing App','2025-07-09 11:59:55'),(7,'com.theappninjas.gpsjoystick','GPS Spoofing App','2025-07-09 11:59:55'),(8,'com.fakegps.mock','GPS Spoofing App','2025-07-09 11:59:55'),(9,'com.mock.location.app','GPS Spoofing App','2025-07-09 11:59:55'),(10,'com.fakegps.location','GPS Spoofing App','2025-07-09 11:59:55'),(11,'com.gpsemulator','GPS Spoofing App','2025-07-09 11:59:55'),(12,'com.locationspoofer','GPS Spoofing App','2025-07-09 11:59:55'),(13,'com.fakegps.pro','GPS Spoofing App','2025-07-09 11:59:55'),(14,'com.mock.gps.location','GPS Spoofing App','2025-07-09 11:59:55'),(15,'com.gps.mock.location','GPS Spoofing App','2025-07-09 11:59:55'),(16,'com.fake.location.spoofer','GPS Spoofing App','2025-07-09 11:59:55'),(17,'com.location.faker','GPS Spoofing App','2025-07-09 11:59:55'),(18,'com.gps.faker','GPS Spoofing App','2025-07-09 11:59:55'),(19,'com.mock.location.faker','GPS Spoofing App','2025-07-09 11:59:55'),(20,'com.location.mock.gps','GPS Spoofing App','2025-07-09 11:59:55'),(21,'com.gps.location.faker','GPS Spoofing App','2025-07-09 11:59:55'),(22,'com.fake.gps.location.spoofer','GPS Spoofing App','2025-07-09 11:59:55'),(23,'com.location.spoofer.gps','GPS Spoofing App','2025-07-09 11:59:55'),(24,'com.hola.mocklocation','Location Simulation App','2025-07-09 12:00:50'),(25,'com.lexa.fakegps.route','Location Simulation App','2025-07-09 12:00:50'),(26,'com.fakegps.mock.location.app','Location Simulation App','2025-07-09 12:00:50'),(27,'com.mock.location.app.free','Location Simulation App','2025-07-09 12:00:50'),(28,'com.location.mock.free','Location Simulation App','2025-07-09 12:00:50'),(29,'com.gps.mock.free','Location Simulation App','2025-07-09 12:00:50'),(33,'com.topjohnwu.magisk','Root Management/Evasion Tool','2025-07-09 12:01:02'),(34,'com.noshufou.android.su','Root Management/Evasion Tool','2025-07-09 12:01:02'),(35,'com.koushikdutta.superuser','Root Management/Evasion Tool','2025-07-09 12:01:02'),(36,'com.zachspong.temprootremovejb','Root Management/Evasion Tool','2025-07-09 12:01:02'),(37,'com.ramdroid.appquarantine','Root Management/Evasion Tool','2025-07-09 12:01:02'),(38,'com.devadvance.rootcloak','Root Management/Evasion Tool','2025-07-09 12:01:02'),(39,'com.devadvance.rootcloakplus','Root Management/Evasion Tool','2025-07-09 12:01:02'),(40,'de.robv.android.xposed.installer','Root Management/Evasion Tool','2025-07-09 12:01:02'),(41,'com.saurik.substrate','Root Management/Evasion Tool','2025-07-09 12:01:02'),(42,'com.amphoras.hidemyroot','Root Management/Evasion Tool','2025-07-09 12:01:02'),(43,'com.amphoras.hidemyrootadfree','Root Management/Evasion Tool','2025-07-09 12:01:02'),(44,'com.formyhm.hiderootPremium','Root Management/Evasion Tool','2025-07-09 12:01:02'),(45,'me.phh.superuser','Root Management/Evasion Tool','2025-07-09 12:01:02'),(46,'eu.chainfire.supersu','Root Management/Evasion Tool','2025-07-09 12:01:02'),(47,'com.kingouser.com','Root Management/Evasion Tool','2025-07-09 12:01:02'),(48,'com.android.vending.billing.InAppBillingService.LOCK','App Cracking/Patching Tool','2025-07-09 12:01:02'),(49,'com.android.vending.billing.InAppBillingService.LACK','App Cracking/Patching Tool','2025-07-09 12:01:02'); - -# -# Structure for table "clock_records" -# - -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` datetime NOT NULL, - `qr_code_id` varchar(255) DEFAULT NULL, - `latitude` decimal(10,8) DEFAULT NULL, - `longitude` decimal(11,8) DEFAULT NULL, - `notes` text, - PRIMARY KEY (`id`), - KEY `worker_id` (`worker_id`), - KEY `qr_code_id` (`qr_code_id`) -) ENGINE=MyISAM AUTO_INCREMENT=106 DEFAULT CHARSET=utf8 COMMENT='Logs every clock-in and clock-out event for all workers.'; - -# -# Data for table "clock_records" -# - -INSERT INTO `clock_records` VALUES (50,6,'clock_in','2025-07-08 14:09:24','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13490670,113.32259360,NULL),(51,6,'clock_out','2025-07-08 15:45:43','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13486030,113.32251780,NULL),(52,6,'clock_in','2025-07-08 15:46:05','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13486030,113.32251780,NULL),(53,4,'failed','2025-07-09 09:50:28','9f72afba-ebb6-445d-a7fc-58df9902777b',37.42199830,-122.08400000,'Clock-in outside of the zone: 11134377.47 meters.'),(54,4,'failed','2025-07-09 09:52:56','9f72afba-ebb6-445d-a7fc-58df9902777b',37.42199830,-122.08400000,'Clock-in outside of the zone: 11134377.47 meters.'),(55,4,'failed','2025-07-09 09:54:43','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13269830,113.32791330,'Clock-in outside of the zone: 392.58 meters.'),(56,4,'failed','2025-07-09 09:57:11','9f72afba-ebb6-445d-a7fc-58df9902777b',23.12999830,113.31499830,'Clock-in outside of the zone: 376.27 meters.'),(57,4,'failed','2025-07-09 09:57:37','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13086670,113.32691330,'Clock-in outside of the zone: 455.18 meters.'),(58,4,'failed','2025-07-09 09:58:01','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13244650,113.32775730,'Clock-in outside of the zone: 399.79 meters.'),(59,4,'failed','2025-07-09 09:58:23','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13244670,113.32784160,'Clock-in outside of the zone: 406.12 meters.'),(60,4,'failed','2025-07-09 09:58:51','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13312330,113.32783670,'Clock-in outside of the zone: 354.82 meters.'),(61,4,'failed','2025-07-09 10:05:28','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13312670,113.32783670,'Clock-in outside of the zone: 354.57 meters.'),(62,4,'failed','2025-07-09 10:13:54','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13312670,113.32783670,'Clock-in outside of the zone: 354.57 meters.'),(63,4,'clock_in','2025-07-09 10:16:51','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13312670,113.32783670,NULL),(64,4,'clock_out','2025-07-09 15:01:53','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13312670,113.32783670,NULL),(65,4,'clock_in','2025-07-09 15:06:55','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13312670,113.32783670,NULL),(66,4,'clock_out','2025-07-09 18:02:51','FORCE_CLOCK_OUT',0.00000000,0.00000000,'Blacklisted App Detected'),(67,4,'failed','2025-07-09 18:08:08','FORCE_CLOCK_OUT',0.00000000,0.00000000,'Forced clock-out failed: User already clocked out.'),(68,4,'clock_out','2025-07-09 18:16:54','FORCE_CLOCK_OUT',0.00000000,0.00000000,'Blacklisted App Detected'),(69,4,'failed','2025-07-09 18:22:09','FORCE_CLOCK_OUT',0.00000000,0.00000000,'FAKE GPS APP Detected.'),(70,4,'failed','2025-07-09 18:23:58','9f72afba-ebb6-445d-a7fc-58df9902777b',37.42198810,-122.08399140,'Clock-in outside of the zone: 11134378.78 meters.'),(71,4,'failed','2025-07-09 18:24:58','9f72afba-ebb6-445d-a7fc-58df9902777b',37.42198810,-122.08399140,'Clock-in outside of the zone: 11134378.78 meters.'),(72,4,'failed','2025-07-09 18:29:30','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13269830,113.32791330,'Clock-in outside of the zone: 392.58 meters.'),(73,4,'failed','2025-07-10 10:13:12','9f72afba-ebb6-445d-a7fc-58df9902777b',37.42199830,-122.08400000,'Clock-in outside of the zone: 11134237.56 meters.'),(74,4,'failed','2025-07-10 10:13:43','9f72afba-ebb6-445d-a7fc-58df9902777b',37.42199830,-122.08400000,'Clock-in outside of the zone: 11134237.56 meters.'),(75,4,'failed','2025-07-10 10:14:45','9f72afba-ebb6-445d-a7fc-58df9902777b',25.21599040,141.62428650,'Clock-in outside of the zone: 2871482.21 meters.'),(76,4,'failed','2025-07-10 10:14:48','9f72afba-ebb6-445d-a7fc-58df9902777b',23.92816100,124.13230650,'Clock-in outside of the zone: 1101840.15 meters.'),(77,4,'clock_in','2025-07-10 10:18:00','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13269830,113.32791330,NULL),(78,4,'clock_out','2025-07-10 10:38:50','FORCE_CLOCK_OUT',NULL,NULL,'Blacklisted App Detected'),(79,4,'failed','2025-07-10 10:59:12','FORCE_CLOCK_OUT',0.00000000,0.00000000,'FAKE GPS APP Detected.'),(80,4,'clock_out','2025-07-10 11:05:34','FORCE_CLOCK_OUT',NULL,NULL,'Blacklisted App Detected'),(81,6,'clock_out','2025-07-10 11:11:04','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13362700,113.32298620,NULL),(82,6,'clock_in','2025-07-10 11:11:44','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13362700,113.32298620,NULL),(83,6,'clock_out','2025-07-10 11:25:20','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13370560,113.32305440,NULL),(84,6,'clock_in','2025-07-10 11:29:30','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13370560,113.32305440,NULL),(85,6,'clock_out','2025-07-10 11:29:34','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13370560,113.32305440,NULL),(86,6,'clock_in','2025-07-10 11:29:53','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13370560,113.32305440,NULL),(87,6,'clock_out','2025-07-10 11:29:58','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13370560,113.32305440,NULL),(88,6,'clock_in','2025-07-10 11:48:36','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13370560,113.32305440,NULL),(89,6,'clock_out','2025-07-10 11:48:40','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13370560,113.32305440,NULL),(90,4,'failed','2025-07-10 13:29:58','FORCE_CLOCK_OUT',0.00000000,0.00000000,'FAKE GPS APP Detected.'),(91,10,'clock_out','2025-07-10 13:31:07','FORCE_CLOCK_OUT',NULL,NULL,'Blacklisted App Detected'),(92,10,'failed','2025-07-10 13:32:06','FORCE_CLOCK_OUT',0.00000000,0.00000000,'FAKE GPS APP Detected.'),(93,10,'clock_out','2025-07-10 13:33:38','FORCE_CLOCK_OUT',NULL,NULL,'Blacklisted App Detected'),(94,10,'failed','2025-07-10 13:40:42','FORCE_CLOCK_OUT',0.00000000,0.00000000,'FAKE GPS APP Detected.'),(95,10,'clock_out','2025-07-10 13:41:01','FORCE_CLOCK_OUT',NULL,NULL,'Blacklisted App Detected'),(96,10,'failed','2025-07-10 13:47:44','FORCE_CLOCK_OUT',0.00000000,0.00000000,'FAKE GPS APP Detected.'),(97,10,'failed','2025-07-10 13:48:00','FORCE_CLOCK_OUT',0.00000000,0.00000000,'FAKE GPS APP Detected.'),(98,10,'clock_in','2025-07-10 14:04:48','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13269830,113.32791330,NULL),(99,4,'clock_in','2025-07-10 14:17:24','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13269830,113.32791330,NULL),(100,4,'clock_out','2025-07-10 14:18:00','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13269830,113.32791330,NULL),(101,4,'clock_in','2025-07-15 08:41:40','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13269830,113.32791330,NULL),(102,4,'clock_out','2025-07-15 08:48:43','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13269830,113.32791330,NULL),(103,4,'clock_in','2025-07-15 08:52:03','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13269830,113.32791330,NULL),(104,4,'clock_out','2025-07-15 08:52:09','9f72afba-ebb6-445d-a7fc-58df9902777b',23.13269830,113.32791330,NULL),(105,10,'clock_out','2025-07-16 09:44:00',NULL,NULL,NULL,'test'); - -# -# Structure for table "geofences" -# - -CREATE TABLE `geofences` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` varchar(100) NOT NULL, - `coordinates` text NOT NULL, - `is_active` tinyint(1) DEFAULT '1', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) -) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; - -# -# Data for table "geofences" -# - -INSERT INTO `geofences` VALUES (1,'Main Work Area','[[113.35311466293217,23.161344441258407],[113.28591534444001,23.161344441258407],[113.28591534444001,23.091366234233973],[113.35311466293217,23.091366234233973],[113.35311466293217,23.161344441258407]]',1,'2025-07-14 16:07:32'); - -# -# Structure for table "location_updates" -# - -CREATE TABLE `location_updates` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `user_id` int(11) NOT NULL, - `longitude` decimal(11,8) NOT NULL COMMENT 'Longitude first for geographic convention', - `latitude` decimal(10,8) NOT NULL COMMENT 'Latitude second for geographic convention', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Single timestamp field', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_created_at` (`created_at`), - KEY `idx_user_created` (`user_id`,`created_at`) COMMENT 'Composite index for user location history' -) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='Optimized for 30-minute updates - essential fields only (longitude, latitude, created_at)'; - -# -# Data for table "location_updates" -# - -INSERT INTO `location_updates` VALUES (1,4,113.32791330,23.13269830,'2025-07-15 16:41:41'),(2,4,113.32791330,23.13269830,'2025-07-15 16:52:04'); - -# -# Structure for table "qr_codes" -# - -CREATE TABLE `qr_codes` ( - `id` varchar(255) NOT NULL COMMENT 'Using the UUID string as the primary key', - `name` varchar(255) NOT NULL, - `is_active` tinyint(1) NOT NULL DEFAULT '1', - `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='Stores all physical QR code locations and their status.'; - -# -# Data for table "qr_codes" -# - -INSERT INTO `qr_codes` VALUES ('19e8f029-2e61-4b34-af3a-ee985f2cff74','Gate A -2',1,'2025-06-20 14:42:29'),('4afb2111-cff8-4706-bc87-44518492d5f6','test',1,'2025-07-02 11:41:21'),('9f72afba-ebb6-445d-a7fc-58df9902777b','GATE A',1,'2025-06-26 15:56:14'),('ASSEMBLY-LINE-1','Assembly Line 1',1,'2025-06-13 13:39:51'),('d654a6bf-2b48-49e9-95c8-4fe9af6c3e44','Gate B',1,'2025-06-13 14:00:31'),('d7ac9594-ad9f-48dc-b984-5a9e7ea7e995','weast',1,'2025-06-20 14:55:09'),('FACTORY-MAIN-ENTRANCE','Factory Main Entrance',1,'2025-06-13 13:39:51'),('WAREHOUSE-SECTION-A','Warehouse Section A',1,'2025-06-13 13:39:51'); - -# -# Structure for table "security_alerts" -# - -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 AUTO_INCREMENT=272 DEFAULT CHARSET=utf8; - -# -# Data for table "security_alerts" -# - - -# -# Structure for table "security_checks" -# - -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 AUTO_INCREMENT=107 DEFAULT CHARSET=utf8; - -# -# Data for table "security_checks" -# - - -# -# Structure for table "workers" -# - -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, - `department` varchar(50) DEFAULT NULL, - `position` varchar(100) DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `username` (`username`), - KEY `idx_device_uuid` (`device_uuid`) -) ENGINE=MyISAM AUTO_INCREMENT=29 DEFAULT CHARSET=utf8 COMMENT='Stores user account information for both workers and managers.'; - -# -# Data for table "workers" -# - -INSERT INTO `workers` VALUES (1,'worker','$2b$10$ej9XEoBLd6Gl0OJvQbScdeACzdHt98VJVsxs7PqV6XSyhfsVCGfNe','John Doe','worker',NULL,'2025-06-13 13:39:51','test','test'),(2,'worker2','$2b$10$SxjhV19fhO1ILISNxxVJXeJ23Z3p/Dclt47c0j7SfuliROKDpANQC','Jane Smith','worker','7cf298e6-7cf2-4cf2-f306-5365d6d7','2025-06-13 13:39:51','test','test'),(3,'manager','$2b$10$GkPmwkSIxv0d6z/R6S/PCe5NLwgKtAOP7/FDCUwR1vCFZ7ex3FeNi','Manager Bob','manager',NULL,'2025-06-13 13:39:51','test','test'),(4,'ryanlee','$2b$10$jsYy.2SzpJ9A0LWu6CpoK.mZ3GZWZoIp8g81sRfKt2G4Dipjp8Sa6','Ryan Lee','worker','557fd11d-557f-457f-f214-14df7cf1','2025-06-13 14:48:06','testb','test'),(6,'modewang','$2b$10$4gb.m2IgY9iJoVjyVUQ9IuUmiKNuR6TlxUsXmiaSpf8XFrnko8bcG','Ryan Qi','worker',NULL,'2025-06-13 15:53:08','testb','test'),(8,'bwilliams','$2b$10$MsJ1baJE.g4tAm1HOhTFD.cz1vd80BRPQE//hNVuikdLf2QOhdLi6','Bob Williams','worker',NULL,'2025-06-13 15:57:30','testb','test'),(10,'dmiller','$2b$10$e0cpp2JcfExeB9APu2Hbf.H21rn8XBcCsJOCuVUjltlBqTOOQJuDm','Diana Miller','worker','3a2641b6-3a26-4a26-ee92-0d1ea028','2025-06-13 15:57:30','testb','test'); - -# -# Structure for table "active_user_devices" -# - -CREATE VIEW `active_user_devices` AS - select `w`.`id` AS `id`,`w`.`id` AS `user_id`,`w`.`device_uuid` AS `device_uuid`,NULL AS `device_info`,NULL AS `registered_at`,NULL AS `last_seen`,(case when (`w`.`device_uuid` is not null) then 1 else 0 end) AS `is_online`,`w`.`created_at` AS `created_at`,`w`.`created_at` AS `updated_at`,`w`.`username` AS `username`,`w`.`full_name` AS `full_name`,`w`.`role` AS `role`,NULL AS `minutes_since_last_seen` from `workers` `w` where ((`w`.`device_uuid` is not null) and (`w`.`role` = 'worker')); - -# -# Structure for table "recent_location_updates" -# - -CREATE VIEW `recent_location_updates` AS - select `lu`.`id` AS `id`,`lu`.`user_id` AS `user_id`,`lu`.`longitude` AS `longitude`,`lu`.`latitude` AS `latitude`,`lu`.`created_at` AS `created_at`,`w`.`username` AS `username`,`w`.`full_name` AS `full_name`,timestampdiff(MINUTE,`lu`.`created_at`,now()) AS `minutes_ago` from (`location_updates` `lu` join `workers` `w` on((`lu`.`user_id` = `w`.`id`))) where (`lu`.`created_at` > (now() - interval 24 hour)) order by `lu`.`created_at` desc; - -# -# Structure for table "security_summary" -# - -CREATE VIEW `security_summary` AS - select `w`.`id` AS `user_id`,`w`.`username` AS `username`,`w`.`full_name` AS `full_name`,`sc`.`risk_level` AS `latest_risk_level`,`sc`.`risk_score` AS `latest_risk_score`,`sc`.`created_at` AS `last_security_check`,count(`sa`.`id`) AS `active_alerts`,`w`.`device_uuid` AS `current_device`,NULL AS `device_last_seen` from ((`workers` `w` left join `security_checks` `sc` on(((`w`.`id` = `sc`.`user_id`) and (`sc`.`id` = (select max(`security_checks`.`id`) from `security_checks` where (`security_checks`.`user_id` = `w`.`id`)))))) left join `security_alerts` `sa` on(((`w`.`id` = `sa`.`user_id`) and (`sa`.`is_resolved` = FALSE)))) where (`w`.`role` = 'worker') group by `w`.`id`,`w`.`username`,`w`.`full_name`,`sc`.`risk_level`,`sc`.`risk_score`,`sc`.`created_at`,`w`.`device_uuid`; diff --git a/geofence_simple.sql b/geofence_simple.sql deleted file mode 100644 index 238ffa8..0000000 --- a/geofence_simple.sql +++ /dev/null @@ -1,13 +0,0 @@ --- Simple geofence management table -CREATE TABLE IF NOT EXISTS `geofences` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `name` varchar(100) NOT NULL, - `coordinates` text NOT NULL, - `is_active` tinyint(1) DEFAULT 1, - `created_at` timestamp DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) -) ENGINE=MyISAM DEFAULT CHARSET=utf8; - --- Insert current geofence as default -INSERT INTO `geofences` (`name`, `coordinates`) VALUES -('Main Work Area', '[[113.35311466293217,23.161344441258407],[113.28591534444001,23.161344441258407],[113.28591534444001,23.091366234233973],[113.35311466293217,23.091366234233973],[113.35311466293217,23.161344441258407]]'); diff --git a/index.html b/index.html index 7652f40..88d4904 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> -