// --- Time Tracker Backend Server (Updated) --- require('dotenv').config(); const express = require('express'); const sqlite3 = require('sqlite3'); const { open } = require('sqlite'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const cors = require('cors'); const path = require('path'); const PORT = process.env.PORT || 3000; const JWT_SECRET = process.env.JWT_SECRET || 'default_secret_key'; const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'admin'; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'adminpassword'; const app = express(); app.use(cors()); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); let db; 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."); 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); } } 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 )`); await db.exec(`CREATE TABLE IF NOT EXISTS notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, admin_username TEXT NOT NULL, employee_user_id INTEGER NOT NULL, note_text TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (employee_user_id) REFERENCES users (id) ON DELETE CASCADE )`); const adminUser = await db.get('SELECT * FROM users WHERE username = ?', [ADMIN_USERNAME]); if (!adminUser) { 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."); } function setupRoutes() { const authenticateToken = (req, res, next) => { const token = req.headers['authorization']?.split(' ')[1]; if (!token) 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(); }); }; const requireRole = (role) => (req, res, next) => { if (req.user && req.user.role === role) next(); else res.status(403).json({ message: "Access denied." }); }; // --- Auth --- 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 { res.status(500).json({ message: "Server error during login." }); } }); // --- Punch --- app.post('/api/punch', authenticateToken, async (req, res) => { try { const { id, username } = req.user; const openPunch = await db.get(`SELECT * FROM time_entries WHERE user_id = ? AND status = 'in' ORDER BY punch_in_time DESC LIMIT 1`, [id]); const now = new Date().toISOString(); if (openPunch) { await db.run(`UPDATE time_entries SET punch_out_time = ?, status = 'out' WHERE id = ?`, [now, openPunch.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, now]); res.json({ message: "Punched in." }); } } catch { res.status(500).json({ message: "Server error during punch." }); } }); 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 { res.status(500).json({ message: "Server error fetching status." }); } }); // --- User --- app.post('/api/user/change-password', authenticateToken, async (req, res) => { try { const { currentPassword, newPassword } = req.body; const user = await db.get('SELECT * FROM users WHERE id = ?', [req.user.id]); if (!user) return res.status(404).json({ message: "User not found." }); const isMatch = await bcrypt.compare(currentPassword, user.password); if (!isMatch) return res.status(401).json({ message: "Incorrect current password." }); const hashed = await bcrypt.hash(newPassword, 10); await db.run('UPDATE users SET password = ? WHERE id = ?', [hashed, req.user.id]); res.json({ message: "Password updated successfully." }); } catch { res.status(500).json({ message: "Error changing password." }); } }); app.post('/api/user/request-time-off', authenticateToken, async (req, res) => { try { const { startDate, endDate, reason } = req.body; const { id, username } = req.user; 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." }); } catch { res.status(500).json({ message: "Failed to submit request." }); } }); app.get('/api/user/time-off-requests', authenticateToken, async (req, res) => { try { // This query now only gets requests that are pending OR have an end date in the future. const rows = await db.all( `SELECT * FROM time_off_requests WHERE user_id = ? AND (status = 'pending' OR end_date >= date('now')) ORDER BY start_date ASC`, [req.user.id] ); res.json(rows); } catch (err) { console.error("Error fetching time off requests:", err); res.status(500).json({ message: "Failed to fetch requests." }); } }); app.get('/api/user/time-off-requests/history', authenticateToken, async (req, res) => { try { // This query gets ALL requests for the user, sorted by newest first. const rows = await db.all( "SELECT * FROM time_off_requests WHERE user_id = ? ORDER BY start_date DESC", [req.user.id] ); res.json(rows); } catch (err) { console.error("Error fetching time off history:", err); res.status(500).json({ message: "Failed to fetch request history." }); } }); // --- Admin Tools --- app.post('/api/admin/force-clock-out', authenticateToken, requireRole('admin'), async (req, res) => { try { const { userId } = req.body; const punch = await db.get(`SELECT * FROM time_entries WHERE user_id = ? AND status = 'in' ORDER BY punch_in_time DESC LIMIT 1`, [userId]); if (!punch) return res.status(404).json({ message: "No active punch-in found." }); await db.run(`UPDATE time_entries SET punch_out_time = ?, status = 'out' WHERE id = ?`, [new Date().toISOString(), punch.id]); res.json({ message: "User has been clocked out." }); } catch { res.status(500).json({ message: "Server error forcing clock out." }); } }); app.post('/api/cleanup/malformed-entries', authenticateToken, requireRole('admin'), async (req, res) => { try { const result = await db.run(` UPDATE time_entries SET status = 'out' WHERE status = 'in' AND punch_out_time IS NOT NULL `); res.json({ message: `Corrected ${result.changes} malformed entries.` }); } catch (err) { console.error("Error during cleanup:", err); res.status(500).json({ message: "Server error during cleanup." }); } }); 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 { res.status(500).json({ message: "Server error fetching logs." }); } }); // Gets all users for the management table app.get('/api/admin/users', authenticateToken, requireRole('admin'), async (req, res) => { try { const users = await db.all("SELECT id, username, role FROM users"); // Add a flag to identify the primary admin to protect them from deletion/demotion const usersWithPrimaryFlag = users.map(u => ({ ...u, isPrimary: u.username === ADMIN_USERNAME })); res.json(usersWithPrimaryFlag); } catch { res.status(500).json({ message: "Failed to fetch users." }); } }); app.post('/api/admin/add-punch', authenticateToken, requireRole('admin'), async (req, res) => { try { const { userId, username, punchInTime, punchOutTime } = req.body; if (!userId || !punchInTime) { return res.status(400).json({ message: "User and Punch In time are required." }); } // SCENARIO 1: A "punch-in only" was submitted if (!punchOutTime) { // First, ensure this user doesn't already have an active punch const existingPunch = await db.get("SELECT id FROM time_entries WHERE user_id = ? AND status = 'in'", [userId]); if (existingPunch) { return res.status(409).json({ message: `${username} is already punched in. Please edit their existing entry.` }); } // If clear, insert the new active punch await db.run( 'INSERT INTO time_entries (user_id, username, punch_in_time, status) VALUES (?, ?, ?, ?)', [userId, username, punchInTime, 'in'] ); return res.status(201).json({ message: `Active punch successfully started for ${username}.` }); } // SCENARIO 2: A complete entry (in and out) was submitted if (new Date(punchOutTime) < new Date(punchInTime)) { return res.status(400).json({ message: "Punch out time cannot be before punch in time." }); } await db.run( 'INSERT INTO time_entries (user_id, username, punch_in_time, punch_out_time, status) VALUES (?, ?, ?, ?, ?)', [userId, username, punchInTime, punchOutTime, 'out'] ); res.status(201).json({ message: `Completed entry added successfully for ${username}.` }); } catch (err) { console.error("Error adding manual punch:", err); res.status(500).json({ message: 'Failed to add manual punch.' }); } }); app.post('/api/admin/reset-password', authenticateToken, requireRole('admin'), async (req, res) => { try { const { username, newPassword } = req.body; if (!username || !newPassword) { return res.status(400).json({ message: "Username and new password are required." }); } 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 successfully.` }); } catch (err) { console.error("Error resetting password:", err); res.status(500).json({ message: 'Failed to reset password.' }); } }); // Gets all time entries for the detailed log view 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 { res.status(500).json({ message: "Server error fetching logs." }); } }); // Gets only PENDING time off requests for the main dashboard view 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 { res.status(500).json({ message: "Failed to fetch pending requests." }); } }); // Gets APPROVED/DENIED requests for the history view 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 { res.status(500).json({ message: "Failed to fetch request history." }); } }); // Gets archived time entries 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"); res.json(rows); } catch { res.status(500).json({ message: "Failed to fetch archives." }); } }); // THIS IS THE NEW/FIXED ROUTE app.put('/api/admin/logs/:id', authenticateToken, requireRole('admin'), async (req, res) => { try { const { id } = req.params; const { punch_in_time, punch_out_time } = req.body; // **THE FIX IS HERE** // Determine the correct status based on the punch_out_time. 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, id] ); res.json({ message: 'Entry updated successfully.' }); } catch (err) { console.error("Error updating log:", err); res.status(500).json({ message: 'Failed to update entry.' }); } }); app.delete('/api/admin/logs/:id', authenticateToken, requireRole('admin'), async (req, res) => { try { const { id } = req.params; const result = await db.run('DELETE FROM time_entries WHERE id = ?', [id]); if (result.changes === 0) { return res.status(404).json({ message: "Entry not found or already deleted." }); } res.json({ message: 'Time entry deleted successfully.' }); } catch (err) { console.error("Error deleting log:", err); res.status(500).json({ message: 'Failed to delete time entry.' }); } }); // Admin creates a note for an employee app.post('/api/admin/notes', authenticateToken, requireRole('admin'), async (req, res) => { try { const { userId, noteText } = req.body; const adminUsername = req.user.username; // Get admin's username from their token if (!userId || !noteText) { return res.status(400).json({ message: "Employee and note text are required." }); } await db.run( 'INSERT INTO notes (admin_username, employee_user_id, note_text) VALUES (?, ?, ?)', [adminUsername, userId, noteText] ); res.status(201).json({ message: "Note successfully posted." }); } catch (err) { console.error("Error posting note:", err); res.status(500).json({ message: 'Failed to post note.' }); } }); // Employee fetches their notes app.get('/api/user/notes', authenticateToken, async (req, res) => { try { const notes = await db.all( "SELECT admin_username, note_text, created_at FROM notes WHERE employee_user_id = ? ORDER BY created_at DESC", [req.user.id] ); res.json(notes); } catch (err) { console.error("Error fetching notes:", err); res.status(500).json({ message: 'Failed to fetch notes.' }); } }); // --- Note Management Routes --- // Admin gets all notes for a specific employee app.get('/api/admin/notes/:userId', authenticateToken, requireRole('admin'), async (req, res) => { try { const { userId } = req.params; const notes = await db.all( "SELECT id, admin_username, note_text, created_at FROM notes WHERE employee_user_id = ? ORDER BY created_at DESC", [userId] ); res.json(notes); } catch (err) { res.status(500).json({ message: 'Failed to fetch notes.' }); } }); // Admin deletes a specific note app.delete('/api/admin/notes/:noteId', authenticateToken, requireRole('admin'), async (req, res) => { try { const { noteId } = req.params; const result = await db.run('DELETE FROM notes WHERE id = ?', [noteId]); if (result.changes === 0) { return res.status(404).json({ message: "Note not found." }); } res.json({ message: 'Note deleted successfully.' }); } catch (err) { res.status(500).json({ message: 'Failed to delete note.' }); } }); } startServer();