All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- 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
234 lines
7.0 KiB
JavaScript
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}`));
|