Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Zapiony/PUCE_UZDI_2026/llms.txt

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

UZDI implements a layered security model: passwords are hashed with bcrypt, all string inputs are sanitized globally before validation using a regex-based pipe, route access is controlled by a Role-Based Access Control (RBAC) system enforced through a NestJS guard and metadata decorator, and the frontend mirrors the same role restrictions in its navigation guard. An audit trail records every DML operation on sensitive tables automatically via PostgreSQL triggers.

Authentication

Password Hashing

Passwords are hashed using bcrypt (version 6.x) with a cost factor of 10 before being stored in seguridad.prsn.prsnpass. Plain-text passwords are never persisted.
// Example hash/compare pattern used in AuthService
import * as bcrypt from 'bcrypt';

const SALT_ROUNDS = 10;

// On registration / password change
const hash = await bcrypt.hash(plainTextPassword, SALT_ROUNDS);

// On login
const isMatch = await bcrypt.compare(plainTextPassword, storedHash);

Token Flow

On a successful POST /api/v1/auth/login, the backend returns a token string that the frontend stores in localStorage under the key uzdi_token.
The current token is a mock string — it is not a signed JWT. For production, implement proper JWT signing and verification using @nestjs/jwt with a secret stored in an environment variable. The mock token is sufficient for development because MockAuthMiddleware performs no cryptographic verification (see below).

Frontend Axios Interceptor

Every outgoing request from the frontend automatically attaches the stored token as a Bearer header via an axios request interceptor defined in src/services/api.ts:
api.interceptors.request.use((config) => {
  const token = localStorage.getItem('uzdi_token')
  if (token) config.headers.Authorization = `Bearer ${token}`
  return config
})
A response interceptor handles 401 Unauthorized responses by clearing localStorage and redirecting to /login:
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem('uzdi_token')
      localStorage.removeItem('uzdi_user')
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

Role-Based Access Control (RBAC)

Role Enum

All valid roles are defined in src/common/enums/rol.enum.ts:
export enum Rol {
  ADMINISTRADOR      = 'Administrador',
  PSICOLOGO          = 'Psicólogo',
  TRABAJADOR_SOCIAL  = 'Trabajador Social',
  EDUCADOR           = 'Educador',
  JURIDICO           = 'Jurídico',
  SUPERADMINISTRADOR = 'Superadministrador',
}

@Roles() Decorator

Defined in src/common/decorators/roles.decorator.ts, this decorator attaches role metadata to a controller class or method handler using NestJS’s SetMetadata:
import { SetMetadata } from '@nestjs/common';
import { Rol } from '../enums/rol.enum';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Rol[]) => SetMetadata(ROLES_KEY, roles);
Apply it to a controller method (or the entire controller class) to restrict access:
// Only Administrators and Super-administrators can access this endpoint
@Roles(Rol.ADMINISTRADOR, Rol.SUPERADMINISTRADOR)
@Delete(':id')
remove(@Param('id') id: string) {
  return this.usersService.remove(+id);
}
If @Roles() is not applied to a handler, RolesGuard allows all authenticated requests through.

RolesGuard

Defined in src/common/guards/roles.guard.ts, this guard reads the roles metadata set by @Roles() and compares it against req.user.rol:
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { Rol } from '../enums/rol.enum';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Rol[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) {
      return true; // No @Roles() decorator → allow through
    }

    const { user } = context.switchToHttp().getRequest();

    if (!user || !user.rol) {
      throw new ForbiddenException('No tienes un rol asignado o no has iniciado sesión.');
    }

    const hasRole = requiredRoles.includes(user.rol);
    if (!hasRole) {
      throw new ForbiddenException(
        `Acceso denegado. Se requiere alguno de estos roles: ${requiredRoles.join(', ')}`
      );
    }

    return true;
  }
}
req.user is populated by MockAuthMiddleware during development (see below) and will be populated by the JWT strategy in production.

MockAuthMiddleware

Defined in src/common/middleware/mock-auth.middleware.ts and applied globally in AppModule, this middleware inspects the Authorization header and injects a mock user object into req.user if a Bearer token is present:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { Rol } from '../enums/rol.enum';

@Injectable()
export class MockAuthMiddleware implements NestMiddleware {
  use(req: Request, _res: Response, next: NextFunction) {
    const auth = req.headers.authorization;
    if (auth?.startsWith('Bearer ')) {
      (req as Request & { user?: { rol: Rol } }).user = { rol: Rol.ADMINISTRADOR };
    }
    next();
  }
}
It is registered in AppModule for all routes:
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(MockAuthMiddleware).forRoutes('*');
  }
}
MockAuthMiddleware must be disabled or replaced before production deployment. In its current form, any request with any Bearer token (including the literal string Bearer fake) will be treated as an ADMINISTRADOR. Replace it with a proper JWT verification strategy (@nestjs/passport + passport-jwt) that validates the token signature and extracts real user claims.

SanitizationPipe

Defined in src/common/pipes/sanitization.pipe.ts and registered globally in main.ts before ValidationPipe, this pipe processes every incoming request body, query parameter, and route parameter using pure regex — it does not invoke the dompurify package at runtime. For each value it encounters, it:
  1. Trims leading and trailing whitespace from strings
  2. Strips <script> tags using a regex pattern
  3. Escapes < and > characters to &lt; and &gt;
@Injectable()
export class SanitizationPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    if (metadata.type !== 'body' && metadata.type !== 'query' && metadata.type !== 'param') {
      return value;
    }
    return this.sanitize(value);
  }

  private sanitize(obj: any): any {
    if (typeof obj === 'string') {
      let sanitized = obj.trim();
      sanitized = sanitized.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
      sanitized = sanitized.replace(/</g, '&lt;').replace(/>/g, '&gt;');
      return sanitized;
    }
    if (Array.isArray(obj)) return obj.map(item => this.sanitize(item));
    if (obj !== null && typeof obj === 'object') {
      const result: Record<string, any> = {};
      for (const [key, value] of Object.entries(obj)) {
        result[key] = this.sanitize(value);
      }
      return result;
    }
    return obj;
  }
}
The pipe recurses through arrays and objects, so all string fields in nested DTOs are sanitized. This provides a defense-in-depth layer against XSS injection via form inputs before the data ever reaches business logic or the database.

Frontend RBAC

The frontend mirrors backend role restrictions using a route-level navigation guard in src/router/index.ts. Each protected route has a minimum required tppr_id (person type ID stored in localStorage.uzdi_user):
tppr_idTypeAccess level
1TécnicoStandard user — restricted views
2CoordinadorElevated access — management views
3Administrativo / DirectorFull access
The ROUTE_MIN_ROL map assigns a minimum tppr_id to each route path. The navigation guard reads tppr_id from localStorage.uzdi_user before each navigation and redirects to /app/dashboard if the user’s role does not meet the minimum requirement.
The tppr_id stored in localStorage.uzdi_user is set at login time from the API response. It reflects the seguridad.tipo_persona catalog entry for the logged-in user, not the Rol enum used on the backend. The two systems are complementary: the backend Rol enum controls API endpoint access; the frontend tppr_id controls route and UI visibility.

Audit Trail

Every DML operation (INSERT, UPDATE, DELETE) on audited tables is automatically logged to seguridad.historial by the fn_registrar_historial PostgreSQL trigger function. Application code does not need to write audit records manually. The historial table schema:
ColumnTypeDescription
hist_idSERIALAuto-incremented primary key
tabla_afectadaVARCHAR(100)Fully qualified table name: 'schema.table'
registro_idINT4Primary key value of the affected row
usua_idINT4Acting user — from app.usuario_id session setting, or 0 if unset
fechaTIMESTAMPTimestamp of the operation (NOW())
accionVARCHAR(100)'INSERT', 'UPDATE', or 'DELETE'
estado_anteriorTEXTJSON snapshot of the row before the operation (NULL on INSERT)
estado_nuevoTEXTJSON snapshot of the row after the operation (NULL on DELETE)
descripcionTEXTAlways 'Registro automático generado por trigger'
To attribute an audit record to a specific user, the application must set the app.usuario_id session variable before executing the DML:
SET LOCAL app.usuario_id = 42;
UPDATE adolescente.expediente SET estado = 'Atraso' WHERE expe_id = 17;
-- seguridad.historial now contains a row with usua_id = 42
In a TypeORM service, this can be done inside a transaction using the query runner:
await queryRunner.query(`SET LOCAL app.usuario_id = ${userId}`);
await queryRunner.manager.update(Expediente, id, updateDto);

Build docs developers (and LLMs) love