Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ProyectoFerretek/FerreMarket/llms.txt

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

FerreMarket uses a three-tier role-based access control (RBAC) system to govern what each user can see and do inside the application. Roles are stored in the usuarios table in Supabase and loaded into memory at startup by cargarPermisosTrabajador(). All permission checks across the app are synchronous comparisons against this in-memory object — there are no per-request server-side checks from the React client beyond the initial load. Understanding how roles are assigned, loaded, and evaluated is essential for both configuring user accounts correctly and extending the permission system.

Available Roles

FerreMarket defines exactly three roles. Every Usuario record must have one of the following values in its rol column:
RoleDisplay NameDescriptionKey Permissions
adminAdministradorFull system access.Manage users, create/edit/delete records in all modules, view all reports, trigger password recovery, access the Usuarios page.
usuarioUsuarioStandard store operator.Create and manage sales, products, clients, and promotions. Cannot access the Usuarios management page or perform user-related actions.
clienteClienteCustomer-facing role.Reserved for future customer portal features. Currently has the most limited access within the admin panel.
Role values in the Supabase usuarios table are stored in lowercase (admin, usuario, cliente). When cargarPermisosTrabajador() loads the data it passes the value through capitalizeWords(), which means the in-memory usuarioActual.rol is capitalised (e.g. 'Admin', 'Usuario', 'Cliente'). All permission guard functions compare against the capitalised form. Passing a lowercase string will cause the check to fail silently — see the note at the bottom of this page.

Permission Guards

All permission logic is centralised in src/utils/auth.ts. The module maintains a private usuarioActual object that is populated once at startup and read synchronously by every guard function.
// Internal state — populated by cargarPermisosTrabajador()
const usuarioActual = {
  id:     '',
  rol:    '',
  estado: '',
  nombre: '',
};
The following functions are exported for use throughout the application:

esAdministrador()

export const esAdministrador = (): boolean => {
  return usuarioActual.rol === 'Admin';
};
Returns true if the current user’s role is 'Admin' (capitalised). This is the base check used by all other guards. Components and pages that are limited to administrators should call this function first.

puedeGestionarUsuarios()

export const puedeGestionarUsuarios = (): boolean => {
  return esAdministrador() && usuarioActual.estado === 'Activo';
};
Returns true only when the current user is both an administrator and has an active account (estado === 'Activo'). This is the guard evaluated at the top of GestionUsuarios — if it returns false, the entire Users page is replaced with an access-denied screen.

puedeRealizarAccion(accion)

export const puedeRealizarAccion = (
  accion: 'crear' | 'editar' | 'eliminar' | 'ver' | 'recuperar'
): boolean => {
  if (!esAdministrador()) return false;

  switch (accion) {
    case 'ver':
    case 'crear':
    case 'editar':
    case 'recuperar':
      return true;
    case 'eliminar':
      return usuarioActual.estado === 'Activo' && usuarioActual.rol === 'Admin';
    default:
      return false;
  }
};
Evaluates whether the current user may perform a specific action. All actions require the Admin role; eliminar additionally requires estado === 'Activo'. The accepted action values are:
ActionDescription
'ver'View a resource’s detail or open it in read-only mode.
'crear'Create a new record (e.g. add a new user).
'editar'Modify an existing record.
'recuperar'Trigger a password recovery email for a user.
'eliminar'Permanently delete a record. Requires active admin status.

obtenerUsuarioActual()

export const obtenerUsuarioActual = () => {
  return usuarioActual;
};
Returns the current in-memory user object ({ id, rol, estado, nombre }). Useful for displaying the logged-in user’s name or role in UI components without re-querying the database.

cargarPermisosTrabajador()

export const cargarPermisosTrabajador = async () => {
  const session = await supabase.auth.getSession();
  if (session.data.session) {
    const { user } = session.data.session;
    const { data, error } = await supabase
      .from('usuarios')
      .select('*')
      .eq('uid', user.id)
      .single();

    if (data) {
      usuarioActual.rol    = capitalizeWords(data.rol)    || 'Usuario';
      usuarioActual.estado = capitalizeWords(data.estado) || 'Activo';
      usuarioActual.nombre = capitalizeWords(data.nombre) || 'Usuario';
    }
  }
};
An async function that fetches the current user’s record from the usuarios table using their Supabase Auth UUID (uid). It then capitalises the rol, estado, and nombre values via the private capitalizeWords helper before storing them in usuarioActual. Must be called after an active session is confirmed.

obtenerUsuarioIdByUUID(uuid)

export const obtenerUsuarioIdByUUID = async (uuid: string): Promise<number | null> => {
  const { data, error } = await supabase
    .from('usuarios')
    .select('id')
    .eq('uid', uuid)
    .single();

  return data ? data.id : null;
};
Resolves a Supabase Auth UUID to the internal numeric id in the usuarios table. Returns null if no matching record is found or if an error occurs. Useful for operations that need the database-level ID (e.g. linking sales or audit records to a user) when only the Auth UUID is available from the session.

Calling cargarPermisosTrabajador

This function must be called once at application startup, after Supabase Auth has confirmed there is an active session. Calling it too early (before the session resolves) will result in an empty usuarioActual and all permission guards returning false.
import { cargarPermisosTrabajador } from '../utils/auth';
import supabase from '../lib/supabase/Supabase';

// Example: call after confirming a session exists
const initApp = async () => {
  const { data: { session } } = await supabase.auth.getSession();
  if (session) {
    await cargarPermisosTrabajador();
    // Permission guards are now ready to use
  }
};
Once loaded, all guard calls (esAdministrador(), puedeGestionarUsuarios(), etc.) are synchronous and safe to call anywhere in the component tree.

Checking Permissions in a Component

Import and call the relevant guard function directly in your component logic:
import { puedeGestionarUsuarios } from '../utils/auth';

const AdminOnlySection = () => {
  if (!puedeGestionarUsuarios()) {
    return (
      <p>You do not have permission to view this section.</p>
    );
  }

  return <div>Admin content here</div>;
};
For action-level checks, use puedeRealizarAccion to conditionally render buttons:
import { puedeRealizarAccion } from '../utils/auth';

const UserActions = ({ userId }: { userId: string }) => (
  <div>
    {puedeRealizarAccion('editar') && (
      <button onClick={() => openEditModal(userId)}>Edit</button>
    )}
    {puedeRealizarAccion('eliminar') && (
      <button onClick={() => confirmDelete(userId)}>Delete</button>
    )}
  </div>
);

Protected Routes

At the routing level, FerreMarket uses a ProtectedRoute wrapper component defined in src/App.tsx. It reads the session value from AuthContext and redirects unauthenticated visitors to /admin/login:
const ProtectedRoute = ({ children }) => {
  const { session } = UserAuth();

  if (session) {
    return <>{children}</>;
  } else {
    return <Navigate to="/admin/login" replace />;
  }
};
All admin routes (dashboard, products, clients, sales, promotions, reports, and users) are nested under this wrapper:
<Route path="/admin" element={<ProtectedRoute><Layout /></ProtectedRoute>}>
  <Route index element={<Dashboard />} />
  <Route path="usuarios" element={<GestionUsuarios />} />
  {/* ...other routes */}
</Route>
ProtectedRoute handles authentication (is there a valid session?). Role-based authorisation (can this authenticated user see this page?) is handled separately by the puedeGestionarUsuarios() and puedeRealizarAccion() guards inside each page component.
Role capitalisation matters. The usuarios table stores roles as lowercase strings (admin, usuario, cliente), but cargarPermisosTrabajador capitalises them before storing in usuarioActual. All guard functions check for the capitalised value — 'Admin', not 'admin'. If you query usuarioActual.rol directly and compare against the raw database value, the comparison will always be false. Always use the exported guard functions rather than inspecting usuarioActual directly.

Build docs developers (and LLMs) love