feat(安全): 实现JWT认证和HTTPS支持
- 添加JWT认证中间件保护API端点 - 在登录流程中使用bcrypt哈希密码和JWT令牌 - 配置HTTPS服务器使用自签名证书 - 更新前端API调用以包含认证令牌
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIID1zCCAr+gAwIBAgIUBR7JyWNONICsFp/nQvPR29Z5PBAwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwezELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM
|
||||||
|
DVNhbi1GcmFuY2lzY28xEzARBgNVBAoMCk15LUNvbXBhbnkxFjAUBgNVBAsMDU15
|
||||||
|
LURlcGFydG1lbnQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNTA2MjYwMjAyMjFa
|
||||||
|
Fw0yNjA2MjYwMjAyMjFaMHsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9y
|
||||||
|
bmlhMRYwFAYDVQQHDA1TYW4tRnJhbmNpc2NvMRMwEQYDVQQKDApNeS1Db21wYW55
|
||||||
|
MRYwFAYDVQQLDA1NeS1EZXBhcnRtZW50MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEi
|
||||||
|
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCRiQgTu/x1V9NTVf4zJ7PQgTO
|
||||||
|
ivda/SNi4HdSoiiyzDfXtyeXXfQwuHR+nGtqr+iideuDtEubig5Zru4gRY1oKGcn
|
||||||
|
mnUdjSpjIaH655gjyCV+tuYC774Fdn4sFZiPAe5ExAwOEgOHsrgGeuLYIIIXX/0J
|
||||||
|
FW2mQWOB0wHzxftNgKC1WoutZ4uNq15nC/W7BfpyR7pUkOVAb0oCNdKsIB4NDq9/
|
||||||
|
is6MW/ZrLTZtsTAP0/f76BKqpM36eeUApvGe4RDQYNGxMVp2SO5oemjOyIz2qXZp
|
||||||
|
SZsSilDhJWqytxd8G3KbdGoZYBmdtuv+Tu48aZMiImpp1+zNDJGiZlzYqd2ZAgMB
|
||||||
|
AAGjUzBRMB0GA1UdDgQWBBQtH8QZY8Ew389MUw+cBocOb4pCfTAfBgNVHSMEGDAW
|
||||||
|
gBQtH8QZY8Ew389MUw+cBocOb4pCfTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
|
||||||
|
DQEBCwUAA4IBAQBwVkWIfqpxAPyEoDQY7XMZrZQ7C6N7VDb5sMFoutTItmlbZOto
|
||||||
|
U08x5HfTfSuqUA2jK0FYhWLFea2njsa8ycffFGbf1YzOTY8956ufSOQrLtzJsZ74
|
||||||
|
aIOr+VffhcdU1HQ3ncp6XY/XySK/C0tyZlJUvw5SIllMQog1jA8B4EX3LSGd0V3Q
|
||||||
|
Hu2FI3G/+iNDk2w4PF9iKFk2p//1Wq+xOjhrxmjyTxKa4w6mM/hwhUBz4nbd5HYo
|
||||||
|
/TyjW7TjAGRYYXOw+NLwoVyLzaSVRVEppGmxFHbuicxfbdHbQAzdhUU7Z2rEp2qA
|
||||||
|
FilIQHQH3kKnifEtfG8zwfAu1rN7c02gdpu3
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
import bcrypt from 'bcrypt'
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
|
dotenv.config({ path: '../.env' })
|
||||||
|
|
||||||
|
async function hashPasswords() {
|
||||||
|
const db = await mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
port: process.env.DB_PORT,
|
||||||
|
})
|
||||||
|
|
||||||
|
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-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCRiQgTu/x1V9N
|
||||||
|
TVf4zJ7PQgTOivda/SNi4HdSoiiyzDfXtyeXXfQwuHR+nGtqr+iideuDtEubig5Z
|
||||||
|
ru4gRY1oKGcnmnUdjSpjIaH655gjyCV+tuYC774Fdn4sFZiPAe5ExAwOEgOHsrgG
|
||||||
|
euLYIIIXX/0JFW2mQWOB0wHzxftNgKC1WoutZ4uNq15nC/W7BfpyR7pUkOVAb0oC
|
||||||
|
NdKsIB4NDq9/is6MW/ZrLTZtsTAP0/f76BKqpM36eeUApvGe4RDQYNGxMVp2SO5o
|
||||||
|
emjOyIz2qXZpSZsSilDhJWqytxd8G3KbdGoZYBmdtuv+Tu48aZMiImpp1+zNDJGi
|
||||||
|
ZlzYqd2ZAgMBAAECggEAJrZJ4BtCsnahezqoh0Yk9ilSYmLsJ9gt6zfN5ywzfm91
|
||||||
|
yDkxvTamYGf9JLagK/36Fhd30wKEF24i7mmOf2VEqIb2iTx8zmLxGZhb2e640RaC
|
||||||
|
rDWfb2HdBGMogLnb8NFCbLcBLPOXevyYsYfeuJD/w08oEZ2QwDKi9L16k9w1d6uK
|
||||||
|
+aebcVggNWOAlDeOtJCn1nqy0TnLl8VSlM42SZKIgFdkXrHWKpfrr+GX1ckj70No
|
||||||
|
/kEHOnMzyT4s0YKvIE114Wm5oIvjACVGO09JJVeolYY/1/y7waMgZmpCD3vp14wF
|
||||||
|
1pZGLQsH4SA7EGv2hRKSENpmuxULdH+tI5A7TvbMtQKBgQD0ec6Jb4TuoKmRftDp
|
||||||
|
GNTeke6x9vyZyyQF+9Z2ZeFwA8k0Agg0r6BWwjX0xNjNv97T8RX2qj26q9EpquEl
|
||||||
|
Uql5y7pTrKaDtGGnI7en5gz3ZhmO4fwxM2TyRHzePXCzbAFvHdq48iI8IPQDLfaj
|
||||||
|
mD/sMBtE24I0W+r4c4lXj7PDJQKBgQDLbocXZjFE0y0OE+sIFVFfU9fD0gOGiQpu
|
||||||
|
i83bCorqEB1sqzIaq3bMDMdVMj9v+Pt2biiIx2JF4QuFC8B2nksFzUjc4Ya567xU
|
||||||
|
qSHqwQfhYqe+g7VpjP4EEefQFCSN/pHFODuqh/Qnl+FBiKvIH8Cx9XlJyFgrjLcG
|
||||||
|
L3+DGlBgZQKBgQCgEMRejaqOhpifT1W+2Gg/VXLgCGM42p06Ybn4CRqCOahZqd7v
|
||||||
|
h/HF/CBRozSD+dUOFTIZLzt9ZhNrYZtJ0YZu3SmgsVDQuKCbSzJq9p+Ut7+SInn5
|
||||||
|
SLoWOSs8YyPQBa4mr3hOURKDJdw93LE4SW3I2XJxMftdBrWgeBz7PHYpFQKBgGhC
|
||||||
|
eVHwugeAA3NXmflGk7G7krV1iQEGcyY82IAOYyuanrn283Lftb7WPcLYQdLtVFNa
|
||||||
|
GcQgC6mssO67GySv89tBXrp1i0r2Gkt5czyROay1lyr47Zzu+bC0TI5EBIgRlDvz
|
||||||
|
mgDINMWl/XhMx12FiIisOkEqQKXxNEwjQ8K5VcBRAoGANpRsF9FaMdObg7vLS0iI
|
||||||
|
AKyr8qYVbiXzxKudxBM2DspBo9Ot5Mr68zEQPxd73ErK5WTnf0U7VCG1+/Ikq46H
|
||||||
|
5IallJBUegQaGyPOETRQDakzQNQrvcjh1knqBjBj5mvEjDZnvxa3pg+IC+4IvSGm
|
||||||
|
7WaDkfSnenvCVQ3tx+XQUaw=
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
+59
-21
@@ -1,9 +1,13 @@
|
|||||||
|
import https from 'https'
|
||||||
|
import fs from 'fs'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import { Parser } from 'json2csv'
|
import { Parser } from 'json2csv'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import mysql from 'mysql2/promise'
|
import mysql from 'mysql2/promise'
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv'
|
||||||
|
import bcrypt from 'bcrypt'
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
|
||||||
// Main function to start the server
|
// Main function to start the server
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
@@ -43,14 +47,21 @@ async function startServer() {
|
|||||||
app.post('/api/auth/login', async (req, res) => {
|
app.post('/api/auth/login', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, password } = req.body
|
const { username, password } = req.body
|
||||||
// In a real app, use a secure hashing library like bcrypt to compare passwords
|
|
||||||
const [rows] = await db.execute(
|
const [rows] = await db.execute(
|
||||||
'SELECT id, role FROM workers WHERE username = ? AND password_hash = ?',
|
'SELECT id, role, password_hash FROM workers WHERE username = ?',
|
||||||
[username, password],
|
[username],
|
||||||
)
|
)
|
||||||
if (rows.length > 0) {
|
if (rows.length > 0) {
|
||||||
const user = rows[0]
|
const user = rows[0]
|
||||||
res.json({ message: 'Login successful', role: user.role, userId: user.id })
|
const passwordMatch = await bcrypt.compare(password, user.password_hash)
|
||||||
|
if (passwordMatch) {
|
||||||
|
const token = jwt.sign({ userId: user.id, role: user.role }, process.env.JWT_SECRET, {
|
||||||
|
expiresIn: '1h',
|
||||||
|
})
|
||||||
|
res.json({ message: 'Login successful', token })
|
||||||
|
} else {
|
||||||
|
res.status(401).json({ message: 'Invalid credentials' })
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
res.status(401).json({ message: 'Invalid credentials' })
|
res.status(401).json({ message: 'Invalid credentials' })
|
||||||
}
|
}
|
||||||
@@ -60,8 +71,28 @@ async function startServer() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Middleware to verify JWT
|
||||||
|
const authenticateJWT = (req, res, next) => {
|
||||||
|
const authHeader = req.headers.authorization
|
||||||
|
|
||||||
|
if (authHeader) {
|
||||||
|
const token = authHeader.split(' ')[1]
|
||||||
|
|
||||||
|
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
|
||||||
|
if (err) {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = user
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
res.sendStatus(401)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Worker Clock In/Out Endpoint
|
// Worker Clock In/Out Endpoint
|
||||||
app.post('/api/clock', async (req, res) => {
|
app.post('/api/clock', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { userId, eventType, qrCodeValue, latitude, longitude } = req.body
|
const { userId, eventType, qrCodeValue, latitude, longitude } = req.body
|
||||||
const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [
|
const [qrRows] = await db.execute('SELECT name, is_active FROM qr_codes WHERE id = ?', [
|
||||||
@@ -92,7 +123,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Fetch worker details endpoint
|
// Fetch worker details endpoint
|
||||||
app.get('/api/workers/:id', async (req, res) => {
|
app.get('/api/workers/:id', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
const [rows] = await db.execute(
|
const [rows] = await db.execute(
|
||||||
@@ -111,7 +142,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Worker Status Endpoint
|
// Worker Status Endpoint
|
||||||
app.get('/api/worker/status/:userId', async (req, res) => {
|
app.get('/api/worker/status/:userId', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
const [rows] = await db.execute(
|
const [rows] = await db.execute(
|
||||||
@@ -130,7 +161,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Worker History Endpoint
|
// Worker History Endpoint
|
||||||
app.get('/api/worker/clock-history/:userId', async (req, res) => {
|
app.get('/api/worker/clock-history/:userId', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
// MODIFIED: Use LEFT JOIN and COALESCE to handle manual entries
|
// MODIFIED: Use LEFT JOIN and COALESCE to handle manual entries
|
||||||
@@ -146,7 +177,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Manager: GET All Workers with Search and Pagination
|
// Manager: GET All Workers with Search and Pagination
|
||||||
app.get('/api/managers/workers', async (req, res) => {
|
app.get('/api/managers/workers', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { search = '', page = 1, limit = 20 } = req.query
|
const { search = '', page = 1, limit = 20 } = req.query
|
||||||
const offset = (parseInt(page) - 1) * parseInt(limit)
|
const offset = (parseInt(page) - 1) * parseInt(limit)
|
||||||
@@ -167,15 +198,17 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Manager: POST (Add new) Worker
|
// Manager: POST (Add new) Worker
|
||||||
app.post('/api/managers/workers', async (req, res) => {
|
app.post('/api/managers/workers', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, password, fullName } = req.body
|
const { username, password, fullName } = req.body
|
||||||
if (!username || !password || !fullName) {
|
if (!username || !password || !fullName) {
|
||||||
return res.status(400).json({ message: 'Username, password, and full name are required.' })
|
return res.status(400).json({ message: 'Username, password, and full name are required.' })
|
||||||
}
|
}
|
||||||
|
const saltRounds = 10;
|
||||||
|
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||||
const [result] = await db.execute(
|
const [result] = await db.execute(
|
||||||
"INSERT INTO workers (username, password_hash, full_name, role) VALUES (?, ?, ?, 'worker')",
|
"INSERT INTO workers (username, password_hash, full_name, role) VALUES (?, ?, ?, 'worker')",
|
||||||
[username, password, fullName],
|
[username, hashedPassword, fullName],
|
||||||
)
|
)
|
||||||
res.status(201).json({ id: result.insertId, username, fullName })
|
res.status(201).json({ id: result.insertId, username, fullName })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -188,7 +221,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Manager: DELETE Worker
|
// Manager: DELETE Worker
|
||||||
app.delete('/api/managers/workers/:id', async (req, res) => {
|
app.delete('/api/managers/workers/:id', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
const [result] = await db.execute("DELETE FROM workers WHERE id = ? AND role = 'worker'", [
|
const [result] = await db.execute("DELETE FROM workers WHERE id = ? AND role = 'worker'", [
|
||||||
@@ -207,7 +240,7 @@ async function startServer() {
|
|||||||
// --- NEW --- Manager: POST (Add Manual Attendance Record)
|
// --- NEW --- Manager: POST (Add Manual Attendance Record)
|
||||||
// Note: For this to work, you may need to alter your database table:
|
// Note: For this to work, you may need to alter your database table:
|
||||||
// ALTER TABLE clock_records ADD COLUMN notes TEXT;
|
// ALTER TABLE clock_records ADD COLUMN notes TEXT;
|
||||||
app.post('/api/managers/add-record', async (req, res) => {
|
app.post('/api/managers/add-record', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { workerId, eventType, timestamp, notes } = req.body
|
const { workerId, eventType, timestamp, notes } = req.body
|
||||||
|
|
||||||
@@ -244,7 +277,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Manager: GET Attendance Records
|
// Manager: GET Attendance Records
|
||||||
app.get('/api/managers/attendance-records', async (req, res) => {
|
app.get('/api/managers/attendance-records', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { workerIds, startDate, endDate, format } = req.query
|
const { workerIds, startDate, endDate, format } = req.query
|
||||||
if (!workerIds) {
|
if (!workerIds) {
|
||||||
@@ -286,7 +319,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Manager: GET QR Codes
|
// Manager: GET QR Codes
|
||||||
app.get('/api/managers/qr-codes', async (req, res) => {
|
app.get('/api/managers/qr-codes', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const [rows] = await db.execute(
|
const [rows] = await db.execute(
|
||||||
'SELECT id, name, is_active, created_at FROM qr_codes ORDER BY created_at DESC',
|
'SELECT id, name, is_active, created_at FROM qr_codes ORDER BY created_at DESC',
|
||||||
@@ -299,7 +332,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Manager: POST QR Code
|
// Manager: POST QR Code
|
||||||
app.post('/api/managers/qr-codes', async (req, res) => {
|
app.post('/api/managers/qr-codes', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { name } = req.body
|
const { name } = req.body
|
||||||
if (!name) return res.status(400).json({ message: 'QR Code name is required.' })
|
if (!name) return res.status(400).json({ message: 'QR Code name is required.' })
|
||||||
@@ -319,7 +352,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Manager: PUT QR Code
|
// Manager: PUT QR Code
|
||||||
app.put('/api/managers/qr-codes/:id', async (req, res) => {
|
app.put('/api/managers/qr-codes/:id', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
const { isActive } = req.body
|
const { isActive } = req.body
|
||||||
@@ -338,7 +371,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Manager: DELETE QR Code
|
// Manager: DELETE QR Code
|
||||||
app.delete('/api/managers/qr-codes/:id', async (req, res) => {
|
app.delete('/api/managers/qr-codes/:id', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
const [result] = await db.execute('DELETE FROM qr_codes WHERE id = ?', [id])
|
const [result] = await db.execute('DELETE FROM qr_codes WHERE id = ?', [id])
|
||||||
@@ -351,7 +384,7 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Manager: GET single worker's details
|
// Manager: GET single worker's details
|
||||||
app.get('/api/managers/worker/:id', async (req, res) => {
|
app.get('/api/managers/worker/:id', authenticateJWT, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
const [rows] = await db.execute(
|
const [rows] = await db.execute(
|
||||||
@@ -370,8 +403,13 @@ async function startServer() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// --- Server Start ---
|
// --- Server Start ---
|
||||||
app.listen(port, () => {
|
const httpsOptions = {
|
||||||
console.log(`Server is running on http://localhost:${port}`)
|
key: fs.readFileSync(process.env.SSL_KEY_PATH),
|
||||||
|
cert: fs.readFileSync(process.env.SSL_CERT_PATH),
|
||||||
|
}
|
||||||
|
|
||||||
|
https.createServer(httpsOptions, app).listen(port, () => {
|
||||||
|
console.log(`Server is running on https://localhost:${port}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+1162
-15
File diff suppressed because it is too large
Load Diff
@@ -13,12 +13,16 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor/cli": "^7.4.0",
|
||||||
|
"@capacitor/core": "^7.4.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"body-parser": "^2.2.0",
|
"body-parser": "^2.2.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"html5-qrcode": "^2.3.8",
|
"html5-qrcode": "^2.3.8",
|
||||||
"json2csv": "^6.0.0-alpha.2",
|
"json2csv": "^6.0.0-alpha.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
|
||||||
|
|
||||||
export async function apiFetch(endpoint, options = {}) {
|
export async function apiFetch(endpoint, options = {}) {
|
||||||
|
const token = sessionStorage.getItem('token')
|
||||||
|
|
||||||
const defaultHeaders = {
|
const defaultHeaders = {
|
||||||
'ngrok-skip-browser-warning': 'true',
|
'ngrok-skip-browser-warning': 'true',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...options.headers,
|
...options.headers,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
defaultHeaders['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||||
...options,
|
...options,
|
||||||
headers: defaultHeaders,
|
headers: defaultHeaders,
|
||||||
|
|||||||
@@ -99,6 +99,8 @@
|
|||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { apiFetch } from '@/api.js'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const workers = ref([])
|
const workers = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -131,15 +133,12 @@ watch(searchQuery, () => {
|
|||||||
const fetchWorkers = async (page = currentPage.value) => {
|
const fetchWorkers = async (page = currentPage.value) => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const data = await apiFetch(
|
||||||
`${import.meta.env.VITE_API_BASE_URL}/api/managers/workers?search=${searchQuery.value}&page=${page}&limit=${pageSize.value}`,
|
`/api/managers/workers?search=${searchQuery.value}&page=${page}&limit=${pageSize.value}`,
|
||||||
)
|
)
|
||||||
if (res.ok) {
|
workers.value = data.workers
|
||||||
const data = await res.json()
|
totalWorkers.value = data.totalCount
|
||||||
workers.value = data.workers
|
currentPage.value = page
|
||||||
totalWorkers.value = data.totalCount
|
|
||||||
currentPage.value = page
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorMessage.value = 'Failed to fetch workers.'
|
errorMessage.value = 'Failed to fetch workers.'
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@@ -159,18 +158,12 @@ const addWorker = async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/workers`, {
|
const data = await apiFetch('/api/managers/workers', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(newWorker.value),
|
body: JSON.stringify(newWorker.value),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
await fetchWorkers(1) // Refresh list to the first page
|
||||||
if (res.ok) {
|
newWorker.value = { fullName: '', username: '', password: '' } // Clear form
|
||||||
await fetchWorkers(1) // Refresh list to the first page
|
|
||||||
newWorker.value = { fullName: '', username: '', password: '' } // Clear form
|
|
||||||
} else {
|
|
||||||
errorMessage.value = data.message
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorMessage.value = 'An error occurred while adding the worker.'
|
errorMessage.value = 'An error occurred while adding the worker.'
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@@ -182,16 +175,14 @@ const addWorker = async () => {
|
|||||||
const deleteWorker = async (id) => {
|
const deleteWorker = async (id) => {
|
||||||
if (!confirm('Are you sure you want to delete this worker account?')) return
|
if (!confirm('Are you sure you want to delete this worker account?')) return
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/managers/workers/${id}`, {
|
await apiFetch(`/api/managers/workers/${id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
if (res.ok) {
|
// If the deleted worker was the last on the page, go to the previous page
|
||||||
// If the deleted worker was the last on the page, go to the previous page
|
if (workers.value.length === 1 && currentPage.value > 1) {
|
||||||
if (workers.value.length === 1 && currentPage.value > 1) {
|
await fetchWorkers(currentPage.value - 1)
|
||||||
await fetchWorkers(currentPage.value - 1)
|
} else {
|
||||||
} else {
|
await fetchWorkers(currentPage.value)
|
||||||
await fetchWorkers(currentPage.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorMessage.value = 'Failed to delete worker.'
|
errorMessage.value = 'Failed to delete worker.'
|
||||||
|
|||||||
+13
-8
@@ -44,15 +44,20 @@ const handleLogin = async () => {
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
// --- ALIGNMENT CHANGE ---
|
// Store token and user info in session storage
|
||||||
// Store user info in session storage
|
sessionStorage.setItem('token', data.token)
|
||||||
sessionStorage.setItem('userId', data.userId)
|
try {
|
||||||
sessionStorage.setItem('userRole', data.role)
|
const decodedToken = JSON.parse(atob(data.token.split('.')[1]))
|
||||||
|
sessionStorage.setItem('userId', decodedToken.userId)
|
||||||
|
sessionStorage.setItem('userRole', decodedToken.role)
|
||||||
|
|
||||||
if (data.role === 'worker') {
|
if (decodedToken.role === 'worker') {
|
||||||
router.push('/worker/dashboard')
|
router.push('/worker/dashboard')
|
||||||
} else if (data.role === 'manager') {
|
} else if (decodedToken.role === 'manager') {
|
||||||
router.push('/manager/dashboard')
|
router.push('/manager/dashboard')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error.value = 'Invalid token received from server.'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error.value = data.message
|
error.value = data.message
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import fs from 'fs'
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
@@ -15,4 +16,10 @@ export default defineConfig({
|
|||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
https: {
|
||||||
|
key: fs.readFileSync('./backend/key.pem'),
|
||||||
|
cert: fs.readFileSync('./backend/cert.pem'),
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user