Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Danielings/Pasantia-Proyecto/llms.txt

Use this file to discover all available pages before exploring further.

The Sistema de Inventario Tecnológico uses a stateless JWT-based authentication flow. On a successful login the server signs a token containing the user’s identity and role, then sets it as an HTTP-only cookie named acceso_token. Every subsequent request to a protected endpoint automatically includes this cookie, and the verificarToken middleware validates and decodes it before any route logic runs. Password recovery uses a separate time-limited token stored in MySQL.

Login — POST /api/login

Send the user’s email address and password in the request body. The server looks up the Firestore usuarios collection by correo, confirms the account is active, and verifies the password with bcrypt.compare(). On success, a signed JWT is set as a cookie and a bitacora entry with accion: "Login" is created.
curl -X POST http://localhost:3001/api/login \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{
    "correo": "jperez@empresa.com",
    "password": "secreto123"
  }'

Success Response — HTTP 200

{
  "message": "Login exitoso",
  "token": "<jwt_string>",
  "user": {
    "correo": "jperez@empresa.com",
    "username": "jperez",
    "sede": "Torre Centro",
    "rol": "Administrador"
  }
}
The cookie header set by the server looks like:
Set-Cookie: acceso_token=<jwt_string>; Max-Age=3600; Path=/; HttpOnly; SameSite=Lax

Error Responses

ScenarioHTTP StatusMessage
Email not found or wrong password401"Credenciales inválidas"
Account is inactive403"Usuario se encuentra inactivo"
Server error500"Error en el servidor"
The cookie is set with httpOnly: true and secure: false in the current configuration. This means the cookie will be sent over plain HTTP, which exposes the token to network interception. Set secure: true in any production deployment running behind HTTPS to ensure the cookie is only transmitted over encrypted connections.

Logout — POST /api/logout

Clears the acceso_token cookie from the browser, effectively ending the session. No server-side token invalidation is performed — the session is stateless.
curl -X POST http://localhost:3001/api/logout \
  -b cookies.txt \
  -c cookies.txt

Response — HTTP 200

{
  "message": "Sesión cerrada"
}

Get Session Info — GET /api/me

Returns the lightweight JWT payload for the currently authenticated user. This endpoint is called by the frontend AuthContext on every application load to restore session state without requiring a full Firestore lookup.
curl http://localhost:3001/api/me \
  -b cookies.txt

Response — HTTP 200

{
  "autenticado": true,
  "user": {
    "correo": "jperez@empresa.com",
    "username": "jperez",
    "rol": "Administrador",
    "sede": "Torre Centro"
  }
}
If the cookie is missing or the token has expired, the verificarToken middleware intercepts the request before it reaches the route handler and returns HTTP 401.

JWT Structure

The token is signed using jwt.sign() with the JWT_SECRET environment variable and expires after 1 hour (expiresIn: "1h").

Payload

{
  "id": "aB3kLmNpQrSt",
  "rol": "Administrador",
  "sede": "Torre Centro",
  "username": "jperez",
  "correo": "jperez@empresa.com",
  "iat": 1720000000,
  "exp": 1720003600
}
The id field is the Firestore document ID of the user. The sede field is read from the user’s embedded ubicacion.sede at login time and is used by downstream route handlers to scope inventory queries.

The verificarToken Middleware

Every protected route chains verificarToken as the first middleware in its handler array. The middleware reads the acceso_token cookie, verifies the signature, and attaches the decoded payload to req.user so route handlers can access identity and role information.
// backend/middleware/verificarToken.js
import jwt from "jsonwebtoken";
import dotenv from "dotenv";

dotenv.config();

const verificarToken = (req, res, next) => {
  const token = req.cookies.acceso_token;

  if (!token) {
    return res.status(401).json({ message: "No autorizado, sesión expirada" });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET || "lol");
    req.user = decoded;
    next();
  } catch (error) {
    console.error("Error al verificar token:", error.message);
    return res.status(401).json({ message: "Token inválido" });
  }
};

export default verificarToken;
After verificarToken runs successfully, role-specific middleware such as esSuperAdmin or permitirEscritura can further restrict access based on req.user.rol.
Axios must be configured with withCredentials: true for browsers to include the acceso_token cookie in cross-origin requests. Without this flag, the cookie is silently omitted and every protected endpoint will return HTTP 401. The same applies to the session-check call in AuthContext.

Frontend Auth Pattern — AuthContext & RutaProtegida

The frontend maintains a global authentication state using React Context. On application startup, AuthProvider calls GET /api/me with withCredentials: true. If the cookie is still valid, the returned user object is stored in state and the app renders normally. If the call fails (expired or missing token), user is set to null.
// frontend/src/controllers/AuthContext.jsx
export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [cargando, setCargando] = useState(true);

  useEffect(() => {
    const comprobarSesion = async () => {
      try {
        const response = await Axios.get("http://localhost:3001/api/me", {
          withCredentials: true, // Required to send the acceso_token cookie
        });

        if (response.status === 200) {
          setUser(response.data.user);
        }
      } catch (error) {
        setUser(null); // Token expired or not present
      } finally {
        setCargando(false);
      }
    };

    comprobarSesion();
  }, []);

  const loginGlobal = (userData) => setUser(userData);
  const logoutGlobal = () => setUser(null);

  return (
    <AuthContext.Provider value={{ user, loginGlobal, logoutGlobal, cargando }}>
      {!cargando && children}
    </AuthContext.Provider>
  );
};
The app does not render any children until cargando becomes false, preventing a flash of unauthenticated UI while the session check is in flight.

RutaProtegida — Route Guard

All authenticated routes are wrapped inside the RutaProtegida component defined in App.jsx. It reads user from AuthContext and redirects to /login if no session exists.
// frontend/src/App.jsx
const RutaProtegida = () => {
  const { user, cargando } = useAuth();

  if (cargando) {
    return (
      <div className="flex h-screen items-center justify-center bg-gray-50">
        <p className="text-sm font-semibold text-gray-500">
          Verificando sesión...
        </p>
      </div>
    );
  }

  if (!user) {
    return <Navigate to="/login" replace />;
  }

  return <Outlet />;
};
Routes such as /dashboard, /registro, /bitacora, and /ubicacion are all children of this guard. Public routes (/login, /recuperar-password, /nueva-password) are defined outside of it.

Password Recovery Flow

Password recovery is handled by a separate Express router (recuperarPassword.js) backed by a MySQL database rather than Firestore. The flow consists of three steps:
1

Request a reset link — POST /api/recuperar-password

Send the user’s email address. The server generates a 32-byte cryptographically random token with crypto.randomBytes(32).toString('hex'), stores it alongside a 15-minute expiry timestamp in the MySQL usuarios table, then emails the user a reset link containing the token.
curl -X POST http://localhost:3001/api/recuperar-password \
  -H "Content-Type: application/json" \
  -d '{ "email": "jperez@empresa.com" }'
{ "message": "Enlace de recuperación enviado" }
The same generic response is returned whether the email is registered or not, to prevent user enumeration.
2

Validate the token — GET /api/validar-token?token=<token>

The frontend password-reset page calls this endpoint as soon as it loads (using the token query parameter from the URL) to confirm the link is still valid before showing the new-password form.
curl "http://localhost:3001/api/validar-token?token=<reset_token>"
{ "valid": true }
Returns { "valid": false, "message": "Enlace inválido o expirado." } with HTTP 400 if the token does not exist or its token_expires timestamp has passed.
3

Set the new password — POST /api/restablecer-password

Submit the token and the new password (minimum 6 characters). The server re-validates the token and expiry, bcrypt-hashes the new password (10 rounds), updates the MySQL usuarios row, and clears both reset_token and token_expires to prevent reuse.
curl -X POST http://localhost:3001/api/restablecer-password \
  -H "Content-Type: application/json" \
  -d '{
    "token": "<reset_token>",
    "password": "nuevo_secreto123"
  }'
{
  "message": "Contraseña actualizada correctamente. Ya puedes iniciar sesión."
}
The password recovery flow uses MySQL (via a connection pool configured in backend/config/bd.js), not Firestore. This is a separate data store from the main user collection. If you are deploying or migrating the system, ensure the MySQL instance is accessible and the personas and usuarios tables are properly seeded.
Reset tokens expire after 15 minutes. If a user does not complete the reset within that window, they must request a new link. Attempting to use an expired token on the /nueva-password page will display an error and the form will not be shown.

Build docs developers (and LLMs) love