Skip to main content

System Overview

The Portal Self-Service Backend API is built with a modern, scalable architecture using Node.js and Microsoft SQL Server. The system follows an MVC-style pattern with clear separation of concerns.

Technology Stack

  • Runtime: Node.js (ES Modules)
  • Framework: Express.js v5.1.0
  • Database: Microsoft SQL Server
  • ORM: Sequelize v6.37.7
  • Real-time: Socket.io v4.8.3
  • Authentication: Azure AD (OAuth2 JWT)

Key Features

  • JWT-based authentication
  • Role-based access control (RBAC)
  • Real-time notifications
  • Request approval workflows
  • File upload management
  • Swagger API documentation

Architecture Layers

The application follows a layered architecture pattern:
┌─────────────────────────────────────┐
│         HTTP/WebSocket              │
│      (Express + Socket.io)          │
└─────────────────┬───────────────────┘

┌─────────────────▼───────────────────┐
│         Middleware Layer            │
│  (Auth, User Loading, RBAC)         │
└─────────────────┬───────────────────┘

┌─────────────────▼───────────────────┐
│          Routes Layer               │
│    (API Endpoint Definitions)       │
└─────────────────┬───────────────────┘

┌─────────────────▼───────────────────┐
│        Controllers Layer            │
│   (Request Handling & Validation)   │
└─────────────────┬───────────────────┘

┌─────────────────▼───────────────────┐
│         Services Layer              │
│     (Business Logic & Transactions) │
└─────────────────┬───────────────────┘

┌─────────────────▼───────────────────┐
│          Models Layer               │
│    (Sequelize ORM + SQL Server)     │
└─────────────────────────────────────┘

Core Components

1. Server Initialization

The server bootstraps in server.js, setting up both HTTP and WebSocket servers:
server.js
import app from './src/app.js';
import { poolPromise } from './src/config/db.config.js';
import { initSocket } from './src/config/socket.js';
import http from 'http';

const server = http.createServer(app);
initSocket(server);

await poolPromise; // Wait for database connection
server.listen(PORT);
Key aspects:
  • Creates HTTP server wrapping Express app (server.js:14)
  • Initializes Socket.io on the same server (server.js:17)
  • Ensures database connectivity before accepting requests (server.js:22)

2. Express Application

The Express app is configured in src/app.js:
src/app.js
const app = express();
app.use(cors(corsOptions));
app.use(express.json());

// Swagger documentation
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));

// Static file serving
app.use('/uploads', express.static(path.join(process.cwd(), 'public', 'uploads')));

// API routes
app.use('/api', indexRoutes);
Configuration highlights:
  • CORS enabled for frontend communication (src/app.js:12-17)
  • JSON body parsing (src/app.js:21)
  • Swagger UI at /docs endpoint (src/app.js:24)
  • Static file serving for uploads (src/app.js:28)

3. Database Configuration

Sequelize ORM connects to SQL Server with connection pooling:
src/config/db.config.js
const config = {
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    server: process.env.DB_SERVER,
    database: process.env.DB_DATABASE,
    port: parseInt(process.env.DB_PORT, 10),
    options: {
        trustServerCertificate: true,
        enableArithAbort: true
    },
    pool: {
        max: 10,
        min: 0,
        idleTimeoutMillis: 30000
    }
};
Database features:
  • Connection pooling (max 10 connections) (src/config/db.config.js:13-17)
  • Automatic timezone handling (UTC-6) (src/models/index.js:35)
  • Sequelize ORM with model associations (src/models/index.js:65-69)

Authentication Flow

The API uses Azure AD for authentication with a multi-step middleware chain:
1

Token Validation

The authenticateAndExtract middleware validates the JWT token from Azure AD:
src/middleware/authMiddleware.js
const checkJwtBase = auth({
    issuerBaseURL: process.env.AZURE_ISSUER_BASE_URL,
    audience: process.env.AZURE_AUDIENCE,
});
This middleware:
  • Validates JWT signature using Azure’s JWKS endpoint (src/middleware/authMiddleware.js:13-19)
  • Verifies token issuer and audience claims
  • Extracts user identity from token payload (src/middleware/authMiddleware.js:41-50)
2

User Loading

The cargarUsuario middleware loads the full user record from the database:
src/middleware/cargarUsuario.js
const empleado = await db.Empleado.findOne({
    where: { azure_oid: azureId },
    include: [
        { model: db.Rol_empleado, as: 'rol' },
        { model: db.Vacaciones, as: 'vacaciones' }
    ]
});
req.empleado = empleado;
This step:
  • Finds user by Azure Object ID (src/middleware/cargarUsuario.js:16)
  • Loads role and vacation data (src/middleware/cargarUsuario.js:17-20)
  • Attaches full user object to request (src/middleware/cargarUsuario.js:30)
3

Role-Based Access Control

The verificarRol middleware enforces role-based permissions:
src/routes/solicitudRoutes.js
router.get('/admin/pendientes', 
    verificarRol(PERMISOS.SOLO_ADMINS), 
    solicitudController.getSolicitudesPendientes
);
The system supports three roles:
  • EMPLEADO: Standard employee access
  • ADMIN: HR administrator access
  • SUPER_ADMIN: Full system access

Middleware Chain Example

Here’s how the middleware chain works for protected routes:
src/routes/solicitudRoutes.js
router.use(authenticateAndExtract);  // 1. Validate JWT
router.use(cargarUsuario);            // 2. Load user from DB

router.get('/', solicitudController.getSolicitudes);  // 3. Execute controller

Request Approval Workflow

The system implements a sophisticated approval workflow for employee requests (vacation, absence, profile updates):
1

Request Creation

Employees create requests through the API:
src/controllers/solicitudController.js (line 135)
export const createSolicitud = async (req, res) => {
    const { id_tipo_solicitud, fecha_inicio, fecha_fin, 
            descripcion_solicitud, documento_url } = req.body;
    
    const empleado = req.empleado;
    // Validate and create request...
}
The system:
  • Validates request data and date ranges
  • Calculates requested days for time-off requests
  • Creates request with PENDING status (id_estado_solicitud: 1)
2

Notification Dispatch

Administrators are notified via real-time notifications:
src/controllers/solicitudController.js (line 187)
notificationService.sendNotification({
    destinatarios: destinatariosRRHH,
    id_remitente: id_empleado,
    titulo: 'Nueva Solicitud de Permiso',
    mensaje: `${empleado.nombre} ha solicitado ${tipoSolicitud.tipo.toLowerCase()}.`,
    id_tipo_notificacion: TIPOS_NOTIFICACION.SOLICITUD_NUEVA,
    id_referencia: nuevaSolicitud.id_solicitud
});
Notifications are sent through Socket.io for instant delivery.
3

Admin Review

Admins can approve or reject requests:
src/controllers/solicitudController.js (line 468)
export const aprobarSolicitud = async (req, res) => {
    const { id } = req.params;
    const { retroalimentacion } = req.body;
    
    // Validate request state
    if (solicitud.id_estado_solicitud !== ESTADOS_SOLICITUD.PENDIENTE) {
        return res.status(400).json({ 
            message: 'La solicitud ya ha sido procesada' 
        });
    }
    // Process approval...
}
The approval logic:
  • Validates request is still pending (src/controllers/solicitudController.js:487-489)
  • Prevents self-approval (commented for testing)
  • Handles different request types with specific logic (src/controllers/solicitudController.js:509-572)
4

Transaction Processing

Approvals use database transactions for data consistency:For vacation requests:
  • Deducts days from employee’s vacation balance
  • Updates request status to APPROVED
  • Records approval metadata
For profile updates:
  • Applies proposed changes to employee record
  • Updates request status
  • Maintains audit trail
All state changes are atomic - either all operations succeed or none do, ensuring data integrity.

Real-time Notifications with Socket.io

The system uses Socket.io for instant notification delivery:

Socket Authentication

src/config/socket.js (line 37)
io.use((socket, next) => {
    const token = socket.handshake.auth.token;
    
    jwt.verify(token, getKey, {
        audience: process.env.AZURE_AUDIENCE,
        issuer: process.env.AZURE_ISSUER_BASE_URL
    }, async (err, decoded) => {
        // Validate token and load user
        const empleado = await Empleado.findOne({ 
            where: { correo: userEmail } 
        });
        socket.userId = empleado.id_empleado;
        next();
    });
});
Key features:
  • JWT authentication for WebSocket connections (src/config/socket.js:39-44)
  • User identification via Azure AD token (src/config/socket.js:58-66)
  • Personal rooms for each user (src/config/socket.js:79-84)

Event Flow

src/config/socket.js (line 92)
notificationEmitter.on(EVENTOS_INTERNOS.NOTIFICACION_CREADA, (notificacionData) => {
    const roomDestinatario = `room_${notificacionData.id_usuario}`;
    io.to(roomDestinatario).emit('nueva_notificacion', notificacionData);
});
Notifications are:
  1. Created by the notification service
  2. Emitted via internal EventEmitter
  3. Delivered to user’s Socket.io room
  4. Received by connected clients in real-time

API Routes Structure

Routes are organized by resource type:
src/routes/indexRoutes.js
router.use('/empleados', empleadoRoutes);
router.use('/solicitudes', solicitudRoutes);
router.use('/faq', faqRoutes);
router.use('/comunicados', comunicadoRoutes);
router.use('/documentos', documentacionRoutes);
router.use('/dashboard-admin', dashboardRoutes);
router.use('/notificaciones', notificacionRoutes);
router.use('/vacaciones', vacacionesRoutes);
All routes are prefixed with /api (src/app.js:34).

Route Protection Example

src/routes/solicitudRoutes.js
router.use(authenticateAndExtract);  // All routes require auth
router.use(cargarUsuario);            // Load user data

router.get('/', solicitudController.getSolicitudes);
router.get('/admin/pendientes', 
    verificarRol(PERMISOS.SOLO_ADMINS), 
    solicitudController.getSolicitudesPendientes
);

Service Layer Pattern

Controllers delegate business logic to service modules:
Controller (solicitudController.js)

Service (solicitudService.js) - Business logic & transactions

Model (SolicitudModel.js) - Database operations
This separation ensures:
  • Controllers: Handle HTTP concerns (validation, responses)
  • Services: Implement business rules and transactions
  • Models: Define database schema and relationships
Never put business logic directly in controllers. Always use service layer for reusability and testability.

Data Models

The system uses Sequelize models with associations:
src/models/index.js
// Initialize models
db.Empleado = EmpleadoModel(sequelize);
db.Solicitud = SolicitudModel(sequelize);
db.Vacaciones = VacacionesModel(sequelize);
db.Rol_empleado = RolModel(sequelize);

// Set up associations
Object.keys(db).forEach(modelName => {
    if (db[modelName].associate) {
        db[modelName].associate(db);
    }
});
Key models:
  • Empleado: Employee records with Azure AD integration
  • Solicitud: Request/application records
  • Vacaciones: Vacation balance tracking
  • Notificacion: Notification system

Security Considerations

Authentication

  • JWT tokens validated against Azure AD
  • Token signature verification using JWKS
  • Token expiration enforced
  • User existence verified in local database

Authorization

  • Role-based access control (RBAC)
  • Resource ownership validation
  • Admin-only endpoint protection
  • Self-approval prevention (commented for testing)

Data Protection

  • SQL injection prevention via Sequelize ORM
  • Environment variable configuration
  • Encrypted database connections (TLS)
  • Connection pooling for resource management

Input Validation

  • Request body validation in controllers
  • Date range validation
  • State transition validation
  • File upload restrictions

Performance Optimization

Database Connection Pooling

The application uses connection pooling to manage database resources efficiently:
src/config/db.config.js
pool: {
    max: 10,              // Maximum connections
    min: 0,               // Minimum connections
    idleTimeoutMillis: 30000  // Connection timeout
}

Eager Loading

Related data is loaded efficiently using Sequelize includes:
src/middleware/cargarUsuario.js
const empleado = await db.Empleado.findOne({
    include: [
        { model: db.Rol_empleado, as: 'rol' },
        { model: db.Vacaciones, as: 'vacaciones' }
    ]
});
This prevents N+1 query problems.

Error Handling

The application implements consistent error handling:
try {
    // Business logic
} catch (error) {
    console.error('Error al crear la solicitud:', error);
    return res.status(500).json({ 
        message: 'Error interno del servidor' 
    });
}
HTTP status codes:
  • 200: Success
  • 201: Resource created
  • 400: Bad request / validation error
  • 401: Unauthorized (invalid token)
  • 403: Forbidden (insufficient permissions)
  • 404: Resource not found
  • 500: Internal server error

Next Steps

API Reference

Explore detailed endpoint documentation

Quickstart

Set up your development environment

Authentication Guide

Learn about Azure AD integration

Database Setup

Configure database connections

Build docs developers (and LLMs) love