Blackterz uses a layered security model. No single mechanism is responsible for all protection — JWT authentication, bcrypt hashing, parameterized SQL, rate limiting, anti-spoofing in controllers, and HTTP security headers each address a distinct attack surface. This page describes each layer in full, with the real source code that implements it.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Blackterz2/Proyecto_5to_Semestre/llms.txt
Use this file to discover all available pages before exploring further.
JWT Authentication
All protected API routes run theverificarToken middleware before any controller is reached. The middleware reads the Authorization header, verifies the JWT signature and expiry, and attaches the decoded payload to req.usuario so downstream controllers can read the caller’s identity without trusting the request body.
jwt.verify() performs three checks in one call: it verifies the HMAC-SHA256 signature (proving the token was issued by this server and has not been tampered with), it checks the expiry (exp claim), and it decodes the payload and returns it. If any check fails, it throws and the request never reaches the controller.
bcrypt Password Hashing
Passwords are hashed with bcrypt before being stored. The model layer never sees the plaintext — hashing happens in the controller beforecrearUsuario() is called.
bcrypt.hash() call. This defeats precomputed rainbow table attacks. The 10 round factor means the server performs 1,024 iterations per check — slow enough to frustrate brute-force attempts, fast enough for normal login latency (~100 ms).
The
password column on the usuarios table is VARCHAR(60) — exactly the length of a bcrypt hash. The raw password is never written to the database at any point in the code path.Anti-ID Spoofing
Any endpoint that creates or modifies user-owned resources readsusuario_id exclusively from req.usuario (the verified JWT payload). Any usuario_id present in req.body is silently ignored.
usuario_id in the body, the SQL WHERE usuario_id = ? uses the value from the verified JWT. A user can only modify resources they own.
Rate Limiting
express-rate-limit is applied exclusively to the /api/auth/* router to prevent brute-force attacks against passwords.
NODE_ENV=production): 10 requests per 15 minutes per IP. In development: 100 requests, so the limit does not interfere with local testing or automated test runs. Exceeding the limit returns HTTP 429 Too Many Requests with the JSON error message above.
Security Headers (helmet)
helmet() is the first middleware applied, before cors and express.json, so it runs on every request including static file serving.
| Header | Protection |
|---|---|
X-Content-Type-Options: nosniff | Prevents MIME-type sniffing |
X-Frame-Options: SAMEORIGIN | Blocks clickjacking via iframes |
Strict-Transport-Security | Forces HTTPS on future visits |
X-XSS-Protection | Legacy XSS filter (older browsers) |
Referrer-Policy | Controls referrer information |
contentSecurityPolicy is explicitly disabled because public/index.html loads Chart.js from cdnjs.cloudflare.com. Enabling the default CSP would block that CDN resource and break the progress graph.
CORS
cors() with no arguments allows requests from any origin. This is acceptable during development where the frontend and API share the same origin (localhost:3000).
Soft-Delete Account Guard
The auth controller explicitly checks theactivo flag before comparing passwords. A deactivated account cannot log in even if it still exists in the database with a valid password hash.
activo is 0:
403 Forbidden is used here (not 401 Unauthorized) because the user is identified correctly — they are simply not permitted access.
SQL Injection Prevention
Every query in every model usespool.execute() with parameterized placeholders (?). The mysql2 driver escapes all bound values before constructing the query string. There is no string concatenation in any SQL query across the codebase.
pool.execute() uses true server-side prepared statements, which is safer than pool.query() (client-side escaping). Prefer execute in all model code.File Upload Security
Avatar uploads are handled bymulter with explicit restrictions on file type and size.
File type filter
Only files with extensions
.jpg, .jpeg, .png, .gif, or .webp are accepted. The filter checks the extension of the original filename — files with any other extension are rejected before being written to disk.Size limit
Maximum upload size is 2 MB. Requests exceeding this return a 400 error before the body is fully read.
avatar-{usuarioId}.{ext}, so uploading a new photo overwrites the previous one without accumulating stale files. The filename is derived from req.usuario.usuario_id (the verified JWT value), not from the original filename provided by the client.
Password Reset Tokens
password_resets table with an expiry timestamp. After a token is redeemed, usado is set to 1 — subsequent attempts with the same token are rejected even if the token has not yet expired. This makes every reset token single-use.
