timepulse/server.js
2025-08-02 08:53:43 -04:00

309 lines
12 KiB
JavaScript

// --- 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
)`);
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 {
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 {
res.status(500).json({ message: "Failed to fetch requests." });
}
});
// --- 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." });
}
});
// 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.' });
}
});
// Other admin routes (logs, users, roles, etc.) stay the same...
}
startServer();