import express from "express"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; import bcrypt from "bcryptjs"; import db from "./db.js"; import { signToken, requireAuth } from "./auth.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const app = express(); const PORT = process.env.PORT || 3000; app.use(express.json()); // Serve Vue build in production const distPath = join(__dirname, "../dist"); app.use(express.static(distPath)); // ── Auth ────────────────────────────────────────────────────────────────── app.post("/api/auth/login", (req, res) => { const { username, password } = req.body; if (!username || !password) return res.status(400).json({ error: "Faltan credenciales" }); const user = db .prepare("SELECT * FROM users WHERE username = ?") .get(username.toLowerCase()); if (!user || !bcrypt.compareSync(password, user.passwordHash)) return res.status(401).json({ error: "Usuario o contraseña incorrectos" }); const token = signToken({ id: user.id, username: user.username, role: user.role, }); res.json({ token, user: { id: user.id, username: user.username, role: user.role }, }); }); app.get("/api/auth/me", requireAuth, (req, res) => { res.json({ user: req.user }); }); // ── Users (admin only) ──────────────────────────────────────────────────── app.get("/api/users", requireAuth, (_req, res) => { const users = db .prepare( "SELECT id, username, role, createdAt FROM users ORDER BY createdAt", ) .all(); res.json(users); }); app.post("/api/users", requireAuth, (req, res) => { const { username, password, role = "admin" } = req.body; if (!username || !password) return res.status(400).json({ error: "Faltan campos obligatorios" }); const existing = db .prepare("SELECT id FROM users WHERE username = ?") .get(username.toLowerCase()); if (existing) return res.status(409).json({ error: "El usuario ya existe" }); const passwordHash = bcrypt.hashSync(password, 10); const info = db .prepare( "INSERT INTO users (username, passwordHash, role) VALUES (?, ?, ?)", ) .run(username.toLowerCase(), passwordHash, role); res.status(201).json({ id: info.lastInsertRowid, username, role }); }); app.put("/api/users/:id/password", requireAuth, (req, res) => { const { password } = req.body; if (!password) return res.status(400).json({ error: "Falta la contraseña" }); const passwordHash = bcrypt.hashSync(password, 10); const info = db .prepare("UPDATE users SET passwordHash = ? WHERE id = ?") .run(passwordHash, req.params.id); if (info.changes === 0) return res.status(404).json({ error: "Not found" }); res.json({ ok: true }); }); app.delete("/api/users/:id", requireAuth, (req, res) => { const remaining = db.prepare("SELECT COUNT(*) as n FROM users").get().n; if (remaining <= 1) return res .status(400) .json({ error: "No se puede eliminar el último usuario" }); db.prepare("DELETE FROM users WHERE id = ?").run(req.params.id); res.json({ ok: true }); }); // ── Students ────────────────────────────────────────────────────────────── app.get("/api/students", requireAuth, (_req, res) => { res.json( db.prepare("SELECT * FROM students ORDER BY lastName, firstName").all(), ); }); app.post("/api/students", requireAuth, (req, res) => { const { id, firstName, lastName, age, sex, weight, height, imc } = req.body; db.prepare( ` INSERT INTO students (id, firstName, lastName, age, sex, weight, height, imc) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `, ).run(id, firstName, lastName, age, sex, weight, height, imc); res.status(201).json({ id }); }); app.put("/api/students/:id", requireAuth, (req, res) => { const { firstName, lastName, age, sex, weight, height, imc } = req.body; const info = db .prepare( ` UPDATE students SET firstName=?, lastName=?, age=?, sex=?, weight=?, height=?, imc=? WHERE id=? `, ) .run(firstName, lastName, age, sex, weight, height, imc, req.params.id); if (info.changes === 0) return res.status(404).json({ error: "Not found" }); res.json({ ok: true }); }); app.delete("/api/students/:id", requireAuth, (req, res) => { // CASCADE deletes linked activities automatically (FK ON DELETE CASCADE) db.prepare("DELETE FROM students WHERE id=?").run(req.params.id); res.json({ ok: true }); }); // ── Activities ──────────────────────────────────────────────────────────── app.get("/api/activities", requireAuth, (_req, res) => { res.json( db.prepare("SELECT * FROM activities ORDER BY createdAt DESC").all(), ); }); app.post("/api/activities", requireAuth, (req, res) => { const { id, studentId, type, durationInput, durationUnit, duration, displayDuration, intensity, lifestyle, anxiety, grade, date, } = req.body; db.prepare( ` INSERT INTO activities (id, studentId, type, durationInput, durationUnit, duration, displayDuration, intensity, lifestyle, anxiety, grade, date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, ).run( id, studentId, type, durationInput, durationUnit, duration, displayDuration, intensity, lifestyle, anxiety ?? 0, grade, date, ); res.status(201).json({ id }); }); app.put("/api/activities/:id", requireAuth, (req, res) => { const { type, durationInput, durationUnit, duration, displayDuration, intensity, lifestyle, anxiety, grade, date, } = req.body; const info = db .prepare( ` UPDATE activities SET type=?, durationInput=?, durationUnit=?, duration=?, displayDuration=?, intensity=?, lifestyle=?, anxiety=?, grade=?, date=? WHERE id=? `, ) .run( type, durationInput, durationUnit, duration, displayDuration, intensity, lifestyle, anxiety ?? 0, grade, date, req.params.id, ); if (info.changes === 0) return res.status(404).json({ error: "Not found" }); res.json({ ok: true }); }); app.delete("/api/activities/:id", requireAuth, (req, res) => { db.prepare("DELETE FROM activities WHERE id=?").run(req.params.id); res.json({ ok: true }); }); // ── SPA fallback ────────────────────────────────────────────────────────── app.get("*", (_req, res) => { res.sendFile(join(distPath, "index.html")); }); app.listen(PORT, () => console.log(`SB Sports API running on :${PORT}`));