refactored server to serve index, allowing to just access a port on a local envirnment
This commit is contained in:
parent
a954ec88f9
commit
3168aa5320
479
server.js
479
server.js
@ -1,8 +1,9 @@
|
||||
// --- Time Tracker Backend Server (Corrected) ---
|
||||
// --- Time Tracker Backend Server (Definitive Fix) ---
|
||||
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const sqlite3 = require('sqlite3');
|
||||
const { open } = require('sqlite');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const cors = require('cors');
|
||||
@ -18,275 +19,285 @@ const app = express();
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
const dbPath = path.resolve(__dirname, 'data', 'timetracker.db');
|
||||
const db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) console.error("Error opening database", err.message);
|
||||
else {
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
let db;
|
||||
|
||||
// --- Main Server Function ---
|
||||
async function startServer() {
|
||||
try {
|
||||
const dbPath = path.resolve(__dirname, 'data', 'timetracker.db');
|
||||
db = await open({
|
||||
filename: dbPath,
|
||||
driver: sqlite3.Database
|
||||
});
|
||||
console.log("Connected to the SQLite database.");
|
||||
initializeDatabase();
|
||||
|
||||
await initializeDatabase();
|
||||
setupRoutes();
|
||||
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
|
||||
|
||||
} catch (err) {
|
||||
console.error("FATAL: Could not start server.", err);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
function initializeDatabase() {
|
||||
db.serialize(() => {
|
||||
db.run(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'employee')`);
|
||||
db.run(`CREATE TABLE IF NOT EXISTS time_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, username TEXT, punch_in_time DATETIME NOT NULL, punch_out_time DATETIME, status TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE)`);
|
||||
db.run(`CREATE TABLE IF NOT EXISTS archived_time_entries (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, username TEXT, punch_in_time DATETIME NOT NULL, punch_out_time DATETIME, status TEXT NOT NULL, archived_at DATETIME NOT NULL)`);
|
||||
db.run(`CREATE TABLE IF NOT EXISTS time_off_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, username TEXT, start_date DATE NOT NULL, end_date DATE NOT NULL, reason TEXT, status TEXT NOT NULL DEFAULT 'pending', FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE)`);
|
||||
|
||||
db.get('SELECT * FROM users WHERE username = ?', [ADMIN_USERNAME], (err, row) => {
|
||||
if (!row) {
|
||||
bcrypt.hash(ADMIN_PASSWORD, 10, (err, hashedPassword) => {
|
||||
db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', [ADMIN_USERNAME, hashedPassword, 'admin']);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// --- NEW: Clean up past time-off requests on server start ---
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
db.run(`DELETE FROM time_off_requests WHERE end_date < ?`, [today], function(err) {
|
||||
if (err) {
|
||||
console.error("Error cleaning up past time-off requests:", err.message);
|
||||
} else if (this.changes > 0) {
|
||||
console.log(`Cleaned up ${this.changes} past time-off requests.`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// --- Middleware ---
|
||||
const requireRole = (role) => (req, res, next) => {
|
||||
if (req.user && req.user.role === role) next();
|
||||
else res.status(403).json({ message: "Access denied." });
|
||||
};
|
||||
|
||||
function authenticateToken(req, res, next) {
|
||||
const token = req.headers['authorization']?.split(' ')[1];
|
||||
if (token == null) return res.status(401).json({ message: "Authentication required. No token provided." });
|
||||
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||
if (err) return res.status(403).json({ message: "Access denied. Invalid or expired token." });
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
async function initializeDatabase() {
|
||||
console.log("Initializing database schema...");
|
||||
await db.exec(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'employee')`);
|
||||
await db.exec(`CREATE TABLE IF NOT EXISTS time_entries (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, username TEXT, punch_in_time DATETIME NOT NULL, punch_out_time DATETIME, status TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE)`);
|
||||
await db.exec(`CREATE TABLE IF NOT EXISTS archived_time_entries (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, username TEXT, punch_in_time DATETIME NOT NULL, punch_out_time DATETIME, status TEXT NOT NULL, archived_at DATETIME NOT NULL)`);
|
||||
await db.exec(`CREATE TABLE IF NOT EXISTS time_off_requests (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, username TEXT, start_date TEXT NOT NULL, end_date TEXT NOT NULL, reason TEXT, status TEXT NOT NULL DEFAULT 'pending', FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE)`);
|
||||
|
||||
const adminUser = await db.get('SELECT * FROM users WHERE username = ?', [ADMIN_USERNAME]);
|
||||
if (!adminUser) {
|
||||
console.log("Primary admin user not found, creating one...");
|
||||
const hashedPassword = await bcrypt.hash(ADMIN_PASSWORD, 10);
|
||||
await db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', [ADMIN_USERNAME, hashedPassword, 'admin']);
|
||||
console.log("Primary admin user created.");
|
||||
}
|
||||
console.log("Database initialization complete.");
|
||||
}
|
||||
|
||||
// --- API Routes ---
|
||||
app.post('/api/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
db.get('SELECT * FROM users WHERE username = ?', [username], (err, user) => {
|
||||
if (!user) return res.status(404).json({ message: "User not found." });
|
||||
bcrypt.compare(password, user.password, (err, isMatch) => {
|
||||
function setupRoutes() {
|
||||
const requireRole = (role) => (req, res, next) => {
|
||||
if (req.user && req.user.role === role) next();
|
||||
else res.status(403).json({ message: "Access denied." });
|
||||
};
|
||||
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const token = req.headers['authorization']?.split(' ')[1];
|
||||
if (token == null) return res.status(401).json({ message: "Authentication required. No token provided." });
|
||||
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||
if (err) return res.status(403).json({ message: "Access denied. Invalid or expired token." });
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
// --- User Routes ---
|
||||
app.post('/api/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
const user = await db.get('SELECT * FROM users WHERE username = ?', [username]);
|
||||
if (!user) return res.status(404).json({ message: "User not found." });
|
||||
const isMatch = await bcrypt.compare(password, user.password);
|
||||
if (!isMatch) return res.status(401).json({ message: "Invalid credentials." });
|
||||
const tokenPayload = { id: user.id, username: user.username, role: user.role };
|
||||
const token = jwt.sign(tokenPayload, JWT_SECRET, { expiresIn: '8h' });
|
||||
res.json({ token, user: tokenPayload });
|
||||
});
|
||||
} catch (err) { res.status(500).json({ message: "Server error during login." }); }
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/punch', authenticateToken, (req, res) => {
|
||||
const { id, username } = req.user;
|
||||
db.get(`SELECT * FROM time_entries WHERE user_id = ? ORDER BY punch_in_time DESC LIMIT 1`, [id], (err, last) => {
|
||||
if (last && last.status === 'in') {
|
||||
db.run(`UPDATE time_entries SET punch_out_time = ?, status = 'out' WHERE id = ?`, [new Date().toISOString(), last.id], () => res.json({ message: "Punched out." }));
|
||||
} else {
|
||||
db.run(`INSERT INTO time_entries (user_id, username, punch_in_time, status) VALUES (?, ?, ?, 'in')`, [id, username, new Date().toISOString()], () => res.json({ message: "Punched in." }));
|
||||
}
|
||||
app.post('/api/punch', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id, username } = req.user;
|
||||
const last = await db.get(`SELECT * FROM time_entries WHERE user_id = ? ORDER BY punch_in_time DESC LIMIT 1`, [id]);
|
||||
if (last && last.status === 'in') {
|
||||
await db.run(`UPDATE time_entries SET punch_out_time = ?, status = 'out' WHERE id = ?`, [new Date().toISOString(), last.id]);
|
||||
res.json({ message: "Punched out." });
|
||||
} else {
|
||||
await db.run(`INSERT INTO time_entries (user_id, username, punch_in_time, status) VALUES (?, ?, ?, 'in')`, [id, username, new Date().toISOString()]);
|
||||
res.json({ message: "Punched in." });
|
||||
}
|
||||
} catch (err) { res.status(500).json({ message: "Server error during punch." }); }
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/status', authenticateToken, (req, res) => {
|
||||
db.all(`SELECT * FROM time_entries WHERE user_id = ? ORDER BY punch_in_time DESC`, [req.user.id], (err, rows) => {
|
||||
if(err) return res.status(500).json({ message: "Database error."});
|
||||
res.json(rows)
|
||||
app.get('/api/status', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const rows = await db.all(`SELECT * FROM time_entries WHERE user_id = ? ORDER BY punch_in_time DESC`, [req.user.id]);
|
||||
res.json(rows);
|
||||
} catch (err) { res.status(500).json({ message: "Server error fetching status." }); }
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/user/change-password', authenticateToken, (req, res) => {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
const { id } = req.user;
|
||||
db.get('SELECT * FROM users WHERE id = ?', [id], (err, user) => {
|
||||
if (err || !user) return res.status(500).json({ message: "Could not find user." });
|
||||
bcrypt.compare(currentPassword, user.password, (err, isMatch) => {
|
||||
app.post('/api/user/change-password', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
const { id } = req.user;
|
||||
const user = await db.get('SELECT * FROM users WHERE id = ?', [id]);
|
||||
if (!user) return res.status(500).json({ message: "Could not find user." });
|
||||
const isMatch = await bcrypt.compare(currentPassword, user.password);
|
||||
if (!isMatch) return res.status(401).json({ message: "Incorrect current password." });
|
||||
bcrypt.hash(newPassword, 10, (err, hashedPassword) => {
|
||||
db.run('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, id], (err) => {
|
||||
if (err) return res.status(500).json({ message: "Failed to update password." });
|
||||
res.json({ message: "Password updated successfully." });
|
||||
});
|
||||
});
|
||||
});
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
await db.run('UPDATE users SET password = ? WHERE id = ?', [hashedPassword, id]);
|
||||
res.json({ message: "Password updated successfully." });
|
||||
} catch (err) { res.status(500).json({ message: "Server error changing password." }); }
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/user/request-time-off', authenticateToken, (req, res) => {
|
||||
const { startDate, endDate, reason } = req.body;
|
||||
const { id, username } = req.user;
|
||||
db.run(
|
||||
'INSERT INTO time_off_requests (user_id, username, start_date, end_date, reason) VALUES (?, ?, ?, ?, ?)',
|
||||
[id, username, startDate, endDate, reason],
|
||||
function(err) {
|
||||
if (err) return res.status(500).json({ message: "Failed to submit request." });
|
||||
app.post('/api/user/request-time-off', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { startDate, endDate, reason } = req.body;
|
||||
const { id, username } = req.user;
|
||||
if (!startDate || !endDate) return res.status(400).json({ message: "Start and end dates are required." });
|
||||
await db.run(`INSERT INTO time_off_requests (user_id, username, start_date, end_date, reason) VALUES (?, ?, ?, ?, ?)`, [id, username, startDate, endDate, reason]);
|
||||
res.status(201).json({ message: "Time off request submitted." });
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
app.get('/api/user/time-off-requests', authenticateToken, (req, res) => {
|
||||
db.all('SELECT * FROM time_off_requests WHERE user_id = ? ORDER BY start_date DESC', [req.user.id], (err, rows) => {
|
||||
if (err) return res.status(500).json({ message: "Failed to retrieve requests." });
|
||||
res.json(rows);
|
||||
} catch (err) { res.status(500).json({ message: "Failed to submit request." }); }
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/user/time-off-requests', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { id } = req.user;
|
||||
const rows = await db.all("SELECT * FROM time_off_requests WHERE user_id = ? ORDER BY start_date DESC", [id]);
|
||||
res.json(rows);
|
||||
} catch (err) { res.status(500).json({ message: "Failed to fetch requests." }); }
|
||||
});
|
||||
|
||||
// --- Admin Routes ---
|
||||
app.post('/api/admin/create-user', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
const { username, password, role } = req.body;
|
||||
const userRole = (role === 'admin' || role === 'employee') ? role : 'employee';
|
||||
bcrypt.hash(password, 10, (err, hashedPassword) => {
|
||||
db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', [username, hashedPassword, userRole], function(err) {
|
||||
if (err) return res.status(409).json({ message: "Username already exists." });
|
||||
// --- Admin Routes ---
|
||||
app.post('/api/admin/create-user', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const { username, password, role } = req.body;
|
||||
const userRole = (role === 'admin' || role === 'employee') ? role : 'employee';
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
await db.run('INSERT INTO users (username, password, role) VALUES (?, ?, ?)', [username, hashedPassword, userRole]);
|
||||
res.status(201).json({ message: `User '${username}' created as ${userRole}.` });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.delete('/api/admin/delete-user/:username', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
const { username } = req.params;
|
||||
if (username === req.user.username) return res.status(400).json({ message: "Cannot delete your own account." });
|
||||
if (username === ADMIN_USERNAME) return res.status(403).json({ message: "The primary admin account cannot be deleted." });
|
||||
|
||||
db.get("SELECT id FROM users WHERE username = ?", [username], (err, userToDelete) => {
|
||||
if (!userToDelete) return res.status(404).json({ message: "User not found." });
|
||||
db.serialize(() => {
|
||||
db.run('BEGIN TRANSACTION');
|
||||
db.run('DELETE FROM time_entries WHERE user_id = ?', [userToDelete.id]);
|
||||
db.run('DELETE FROM archived_time_entries WHERE user_id = ?', [userToDelete.id]);
|
||||
db.run('DELETE FROM time_off_requests WHERE user_id = ?', [userToDelete.id]);
|
||||
db.run('DELETE FROM users WHERE id = ?', [userToDelete.id], (err) => {
|
||||
if(err) {
|
||||
db.run('ROLLBACK');
|
||||
return res.status(500).json({message: "Failed to delete user data."});
|
||||
}
|
||||
db.run('COMMIT', () => res.json({ message: `User '${username}' deleted.` }))
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/admin/reset-password', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
const { username, newPassword } = req.body;
|
||||
if (username === ADMIN_USERNAME) return res.status(403).json({ message: "The primary admin's password cannot be reset by another user." });
|
||||
bcrypt.hash(newPassword, 10, (err, hashedPassword) => {
|
||||
db.run('UPDATE users SET password = ? WHERE username = ?', [hashedPassword, username], function(err) {
|
||||
if (this.changes === 0) return res.status(404).json({ message: "User not found." });
|
||||
res.json({ message: `Password for '${username}' has been reset.` });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/admin/users', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
db.all("SELECT id, username, role FROM users", [], (err, rows) => {
|
||||
if(err) return res.status(500).json({message: "Database error fetching users."});
|
||||
const usersWithFlags = rows.map(row => ({
|
||||
...row,
|
||||
isPrimary: row.username === ADMIN_USERNAME
|
||||
}));
|
||||
res.json(usersWithFlags);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/admin/update-role', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
const { username, newRole } = req.body;
|
||||
if (username === ADMIN_USERNAME) return res.status(403).json({ message: "The primary admin's role cannot be changed." });
|
||||
if (!['admin', 'employee'].includes(newRole)) return res.status(400).json({ message: "Invalid role." });
|
||||
db.get("SELECT role FROM users WHERE username = ?", [username], (err, userToUpdate) => {
|
||||
if (!userToUpdate) return res.status(404).json({ message: "User not found." });
|
||||
if (userToUpdate.role === 'admin' && newRole === 'employee') {
|
||||
db.get("SELECT COUNT(*) as adminCount FROM users WHERE role = 'admin'", (err, row) => {
|
||||
if (row.adminCount <= 1) return res.status(400).json({ message: "Cannot remove the last administrator." });
|
||||
db.run('UPDATE users SET role = ? WHERE username = ?', [newRole, username], () => res.json({ message: `Role for '${username}' updated.` }));
|
||||
});
|
||||
} else {
|
||||
db.run('UPDATE users SET role = ? WHERE username = ?', [newRole, username], () => res.json({ message: `Role for '${username}' updated.` }));
|
||||
} catch (err) {
|
||||
if (err.code === 'SQLITE_CONSTRAINT') return res.status(409).json({ message: "Username already exists." });
|
||||
res.status(500).json({ message: "Server error creating user." });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/admin/add-punch', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
const { userId, username, punchInTime, punchOutTime } = req.body;
|
||||
db.run(`INSERT INTO time_entries (user_id, username, punch_in_time, punch_out_time, status) VALUES (?, ?, ?, ?, 'out')`, [userId, username, punchInTime, punchOutTime], () => res.status(201).json({message: "Time punch added."}));
|
||||
});
|
||||
|
||||
app.post('/api/admin/force-clock-out', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
const { userId } = req.body;
|
||||
db.run(`UPDATE time_entries SET punch_out_time = ?, status = 'out' WHERE user_id = ? AND status = 'in'`, [new Date().toISOString(), userId], function(err) {
|
||||
if(this.changes === 0) return res.status(404).json({message: "No active punch-in found."});
|
||||
res.json({message: "User has been clocked out."});
|
||||
|
||||
app.delete('/api/admin/delete-user/:username', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const { username } = req.params;
|
||||
if (username === req.user.username) return res.status(400).json({ message: "Cannot delete your own account." });
|
||||
if (username === ADMIN_USERNAME) return res.status(403).json({ message: "The primary admin account cannot be deleted." });
|
||||
const userToDelete = await db.get("SELECT id FROM users WHERE username = ?", [username]);
|
||||
if (!userToDelete) return res.status(404).json({ message: "User not found." });
|
||||
await db.run('DELETE FROM users WHERE id = ?', [userToDelete.id]);
|
||||
res.json({ message: `User '${username}' deleted.` });
|
||||
} catch (err) { res.status(500).json({ message: "Server error deleting user." }); }
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/admin/logs', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
db.all(`SELECT * FROM time_entries ORDER BY punch_in_time DESC`, [], (err, rows) => res.json(rows));
|
||||
});
|
||||
app.post('/api/admin/reset-password', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const { username, newPassword } = req.body;
|
||||
if (username === ADMIN_USERNAME) return res.status(403).json({ message: "The primary admin's password cannot be reset by another user." });
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
const result = await db.run('UPDATE users SET password = ? WHERE username = ?', [hashedPassword, username]);
|
||||
if (result.changes === 0) return res.status(404).json({ message: "User not found." });
|
||||
res.json({ message: `Password for '${username}' has been reset.` });
|
||||
} catch (err) { res.status(500).json({ message: "Server error resetting password." }); }
|
||||
});
|
||||
|
||||
app.delete('/api/admin/logs/:id', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
db.run('DELETE FROM time_entries WHERE id = ?', [req.params.id], () => res.json({ message: 'Entry deleted.' }));
|
||||
});
|
||||
app.get('/api/admin/users', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const rows = await db.all("SELECT id, username, role FROM users");
|
||||
const usersWithFlags = rows.map(row => ({ ...row, isPrimary: row.username === ADMIN_USERNAME }));
|
||||
res.json(usersWithFlags);
|
||||
} catch (err) { res.status(500).json({ message: "Server error fetching users." }); }
|
||||
});
|
||||
|
||||
app.put('/api/admin/logs/:id', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
const { punch_in_time, punch_out_time } = req.body;
|
||||
const status = punch_out_time ? 'out' : 'in';
|
||||
db.run(`UPDATE time_entries SET punch_in_time = ?, punch_out_time = ?, status = ? WHERE id = ?`, [punch_in_time, punch_out_time, status, req.params.id], () => res.json({ message: 'Entry updated.' }));
|
||||
});
|
||||
app.post('/api/admin/update-role', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const { username, newRole } = req.body;
|
||||
if (username === ADMIN_USERNAME) return res.status(403).json({ message: "The primary admin's role cannot be changed." });
|
||||
if (!['admin', 'employee'].includes(newRole)) return res.status(400).json({ message: "Invalid role." });
|
||||
const userToUpdate = await db.get("SELECT role FROM users WHERE username = ?", [username]);
|
||||
if (!userToUpdate) return res.status(404).json({ message: "User not found." });
|
||||
if (userToUpdate.role === 'admin' && newRole === 'employee') {
|
||||
const { adminCount } = await db.get("SELECT COUNT(*) as adminCount FROM users WHERE role = 'admin'");
|
||||
if (adminCount <= 1) return res.status(400).json({ message: "Cannot remove the last administrator." });
|
||||
}
|
||||
await db.run('UPDATE users SET role = ? WHERE username = ?', [newRole, username]);
|
||||
res.json({ message: `Role for '${username}' updated.` });
|
||||
} catch (err) { res.status(500).json({ message: "Server error updating role." }); }
|
||||
});
|
||||
|
||||
app.post('/api/admin/archive', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
db.all(`SELECT * FROM time_entries WHERE status = 'out'`, [], (err, rows) => {
|
||||
if (rows.length === 0) return res.json({ message: "No entries to archive." });
|
||||
const archiveTime = new Date().toISOString();
|
||||
db.serialize(() => {
|
||||
db.run('BEGIN TRANSACTION');
|
||||
const insert = db.prepare('INSERT INTO archived_time_entries VALUES (?, ?, ?, ?, ?, ?, ?)');
|
||||
rows.forEach(r => insert.run(r.id, r.user_id, r.username, r.punch_in_time, r.punch_out_time, r.status, archiveTime));
|
||||
insert.finalize();
|
||||
app.post('/api/admin/add-punch', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const { userId, username, punchInTime, punchOutTime } = req.body;
|
||||
await db.run(`INSERT INTO time_entries (user_id, username, punch_in_time, punch_out_time, status) VALUES (?, ?, ?, ?, 'out')`, [userId, username, punchInTime, punchOutTime]);
|
||||
res.status(201).json({message: "Time punch added."});
|
||||
} catch (err) { res.status(500).json({ message: "Server error adding punch." }); }
|
||||
});
|
||||
|
||||
app.post('/api/admin/force-clock-out', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const { userId } = req.body;
|
||||
const result = await db.run(`UPDATE time_entries SET punch_out_time = ?, status = 'out' WHERE user_id = ? AND status = 'in'`, [new Date().toISOString(), userId]);
|
||||
if(result.changes === 0) return res.status(404).json({message: "No active punch-in found."});
|
||||
res.json({message: "User has been clocked out."});
|
||||
} catch (err) { res.status(500).json({ message: "Server error forcing clock out." }); }
|
||||
});
|
||||
|
||||
app.get('/api/admin/logs', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const rows = await db.all(`SELECT * FROM time_entries ORDER BY punch_in_time DESC`);
|
||||
res.json(rows);
|
||||
} catch (err) { res.status(500).json({ message: "Server error fetching logs." }); }
|
||||
});
|
||||
|
||||
app.delete('/api/admin/logs/:id', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
await db.run('DELETE FROM time_entries WHERE id = ?', [req.params.id]);
|
||||
res.json({ message: 'Entry deleted.' });
|
||||
} catch (err) { res.status(500).json({ message: "Server error deleting log." }); }
|
||||
});
|
||||
|
||||
app.put('/api/admin/logs/:id', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const { punch_in_time, punch_out_time } = req.body;
|
||||
const status = punch_out_time ? 'out' : 'in';
|
||||
await db.run(`UPDATE time_entries SET punch_in_time = ?, punch_out_time = ?, status = ? WHERE id = ?`, [punch_in_time, punch_out_time, status, req.params.id]);
|
||||
res.json({ message: 'Entry updated.' });
|
||||
} catch (err) { res.status(500).json({ message: "Server error updating log." }); }
|
||||
});
|
||||
|
||||
app.post('/api/admin/archive', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const rows = await db.all(`SELECT * FROM time_entries WHERE status = 'out'`);
|
||||
if (rows.length === 0) return res.json({ message: "No entries to archive." });
|
||||
const archiveTime = new Date().toISOString();
|
||||
await db.run('BEGIN TRANSACTION');
|
||||
const insert = await db.prepare('INSERT INTO archived_time_entries VALUES (?, ?, ?, ?, ?, ?, ?)');
|
||||
for (const r of rows) {
|
||||
await insert.run(r.id, r.user_id, r.username, r.punch_in_time, r.punch_out_time, r.status, archiveTime);
|
||||
}
|
||||
await insert.finalize();
|
||||
const idsToDelete = rows.map(r => r.id);
|
||||
db.run(`DELETE FROM time_entries WHERE id IN (${idsToDelete.map(() => '?').join(',')})`, idsToDelete);
|
||||
db.run('COMMIT', () => res.json({ message: `Archived ${rows.length} entries.` }));
|
||||
});
|
||||
await db.run(`DELETE FROM time_entries WHERE id IN (${idsToDelete.map(() => '?').join(',')})`, idsToDelete);
|
||||
await db.run('COMMIT');
|
||||
res.json({ message: `Archived ${rows.length} entries.` });
|
||||
} catch (err) {
|
||||
await db.run('ROLLBACK');
|
||||
res.status(500).json({ message: "Server error archiving entries." });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/admin/archives', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
db.all(`SELECT * FROM archived_time_entries ORDER BY archived_at DESC, id DESC`, [], (err, rows) => res.json(rows));
|
||||
});
|
||||
|
||||
app.get('/api/admin/time-off-requests', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
db.all('SELECT * FROM time_off_requests ORDER BY start_date DESC', [], (err, rows) => {
|
||||
if (err) return res.status(500).json({ message: "Failed to retrieve time off requests." });
|
||||
res.json(rows);
|
||||
app.get('/api/admin/archives', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const rows = await db.all(`SELECT * FROM archived_time_entries ORDER BY archived_at DESC, id DESC`);
|
||||
res.json(rows);
|
||||
} catch (err) { res.status(500).json({ message: "Server error fetching archives." }); }
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/admin/update-time-off-status', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
const { requestId, status } = req.body;
|
||||
if (!['approved', 'denied'].includes(status)) {
|
||||
return res.status(400).json({ message: "Invalid status." });
|
||||
}
|
||||
db.run('UPDATE time_off_requests SET status = ? WHERE id = ?', [status, requestId], function (err) {
|
||||
if (err) return res.status(500).json({ message: "Failed to update request." });
|
||||
if (this.changes === 0) return res.status(404).json({ message: "Request not found." });
|
||||
res.json({ message: `Request status updated to ${status}.` });
|
||||
app.get('/api/admin/time-off-requests/pending', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const rows = await db.all("SELECT * FROM time_off_requests WHERE status = 'pending' ORDER BY start_date ASC");
|
||||
res.json(rows);
|
||||
} catch (err) { res.status(500).json({ message: "Failed to fetch pending requests." }); }
|
||||
});
|
||||
});
|
||||
|
||||
// --- NEW: Route to delete a time-off request ---
|
||||
app.delete('/api/admin/time-off-requests/:id', authenticateToken, requireRole('admin'), (req, res) => {
|
||||
db.run('DELETE FROM time_off_requests WHERE id = ?', [req.params.id], function(err) {
|
||||
if (err) return res.status(500).json({ message: "Failed to delete request." });
|
||||
if (this.changes === 0) return res.status(404).json({ message: "Request not found." });
|
||||
res.json({ message: 'Request deleted.' });
|
||||
app.get('/api/admin/time-off-requests/history', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const rows = await db.all("SELECT * FROM time_off_requests WHERE status != 'pending' ORDER BY start_date DESC");
|
||||
res.json(rows);
|
||||
} catch (err) { res.status(500).json({ message: "Failed to fetch request history." }); }
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(PORT, () => console.log(`Server running on http://localhost:${PORT}`));
|
||||
app.post('/api/admin/update-time-off-status', authenticateToken, requireRole('admin'), async (req, res) => {
|
||||
try {
|
||||
const { requestId, status } = req.body;
|
||||
if (!requestId || !['approved', 'denied'].includes(status)) {
|
||||
return res.status(400).json({ message: "Request ID and a valid status are required." });
|
||||
}
|
||||
const result = await db.run("UPDATE time_off_requests SET status = ? WHERE id = ?", [status, requestId]);
|
||||
if (result.changes === 0) return res.status(404).json({ message: "Request not found." });
|
||||
res.json({ message: `Request has been ${status}.` });
|
||||
} catch (err) { res.status(500).json({ message: "Failed to update status." }); }
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user