Files
sbsports/server/index.js
alexandrump 898d021ae8
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: add authentication system (login, users, auth middleware)
- server/auth.js: JWT middleware and auth routes
- src/stores/auth.js + useAuthFetch.js: client-side auth state
- src/views/LoginView.vue + UsersView.vue: login and user management UI
- router, sidebar, App.vue: guard routes behind auth
- COOLIFY.md: add real deployment IDs
2026-04-22 01:22:05 +02:00

234 lines
7.0 KiB
JavaScript

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}`));