feat: add authentication system (login, users, auth middleware)
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
This commit is contained in:
alexandrump
2026-04-22 01:22:05 +02:00
parent 0a97e51dc6
commit 898d021ae8
14 changed files with 819 additions and 123 deletions

View File

@@ -1,7 +1,9 @@
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();
@@ -13,15 +15,96 @@ app.use(express.json());
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", (_req, res) => {
app.get("/api/students", requireAuth, (_req, res) => {
res.json(
db.prepare("SELECT * FROM students ORDER BY lastName, firstName").all(),
);
});
app.post("/api/students", (req, res) => {
app.post("/api/students", requireAuth, (req, res) => {
const { id, firstName, lastName, age, sex, weight, height, imc } = req.body;
db.prepare(
`
@@ -32,7 +115,7 @@ app.post("/api/students", (req, res) => {
res.status(201).json({ id });
});
app.put("/api/students/:id", (req, res) => {
app.put("/api/students/:id", requireAuth, (req, res) => {
const { firstName, lastName, age, sex, weight, height, imc } = req.body;
const info = db
.prepare(
@@ -46,7 +129,7 @@ app.put("/api/students/:id", (req, res) => {
res.json({ ok: true });
});
app.delete("/api/students/:id", (req, res) => {
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 });
@@ -54,13 +137,13 @@ app.delete("/api/students/:id", (req, res) => {
// ── Activities ────────────────────────────────────────────────────────────
app.get("/api/activities", (_req, res) => {
app.get("/api/activities", requireAuth, (_req, res) => {
res.json(
db.prepare("SELECT * FROM activities ORDER BY createdAt DESC").all(),
);
});
app.post("/api/activities", (req, res) => {
app.post("/api/activities", requireAuth, (req, res) => {
const {
id,
studentId,
@@ -98,7 +181,7 @@ app.post("/api/activities", (req, res) => {
res.status(201).json({ id });
});
app.put("/api/activities/:id", (req, res) => {
app.put("/api/activities/:id", requireAuth, (req, res) => {
const {
type,
durationInput,
@@ -137,7 +220,7 @@ app.put("/api/activities/:id", (req, res) => {
res.json({ ok: true });
});
app.delete("/api/activities/:id", (req, res) => {
app.delete("/api/activities/:id", requireAuth, (req, res) => {
db.prepare("DELETE FROM activities WHERE id=?").run(req.params.id);
res.json({ ok: true });
});