DoliMiddlewareApi

Backend for Frontend para Dolibarr ERP

Trabajo Fin de Grado — JavierMB — 2026

.NET 10 LTS ASP.NET Core Docker JWT Auth

Agenda

  1. El problema
  2. Patron BFF y arquitectura
  3. .NET 10 y por que LTS
  4. Stack tecnologico
  5. Autenticacion dual
  6. API y endpoints
  7. DTOs, Mappers y CQRS-lite
  8. Testabilidad: IDolibarrApiClient
  9. Seguridad: Rate Limiting y mas
  10. Errores RFC 7807
  11. Docker y despliegue
  12. Lecciones y futuro

El problema

❌ Dolibarr devuelve todo como string (ids, fechas, decimales, estados)
❌ API REST inconsistente (snake_case, unix timestamps, codigos numericos)
❌ Exponer el ERP directamente al navegador = riesgo de seguridad
❌ El frontend tendria que parsear y transformar todo manualmente

✅ El BFF transforma, valida y cachea — frontend recibe datos limpios
✅ JWT propio — credenciales Dolibarr nunca llegan al navegador
✅ Contratos tipados con DataAnnotations y mappers

Patron BFF y arquitectura

Un BFF es una API disenada exclusivamente para un tipo de cliente frontend.

JS Frontend
JavaScript
↔
BFF
.NET 10
↔
Dolibarr
ERP
  • Abstrae la API legacy detras de endpoints limpios y RESTful
  • Transforma tipos (strings a int/decimal/DateOnly, estados numericos a texto)
  • Aplica reglas de negocio y validacion
  • Centraliza autenticacion: el frontend solo maneja JWT

Arquitectura general

Arquitectura

.NET 10 LTS — por que esta eleccion

Elegimos .NET 10 LTS (soporte hasta noviembre 2028). Ventajas clave:

PERFORMANCE

JIT inlining, escape analysis, NativeAOT mas rapido y ligero

C# 14

Field-backed properties, extension types, ?=

ASP.NET CORE 10

OpenAPI 3.1 por defecto, validacion built-in, rate limiting nativo

LTS = Long Term Support — parches de seguridad y soporte durante 3 anos. La eleccion correcta para un TFG.

Stack tecnologico

Backend

.NET 10 LTS
ASP.NET Core Web API
JWT Bearer Auth
Rate Limiting nativo
Health Checks

Patrones

CQRS-lite (command/query)
Service Layer
Static Mappers
ProblemDetails (RFC 7807)
IMemoryCache
Interface segregation

Infra

Docker + Compose
MySQL 8.0
Dolibarr (container)
xUnit + Moq
Swagger / OpenAPI

Estructura del proyecto

Controllers/

AuthController
ClientsController
InvoicesController
DocumentController
✓ [ProducesResponseType]

Services/

Auth/ AuthService, TokenCache, JwtProvider
Clients/ IDolibarrApiClient interfaz
InvoiceService, ClientService, DocumentService

Dtos/

command/ CreateInvoice...
query/ InvoiceDto...
Dolibarr/ *Response...

Otros

Mappers/
Exceptions/
Program.cs

clave IDolibarrApiClient es una interfaz — facilita mocking y testing

Autenticacion dual

Dos capas de autenticacion transparentes para el frontend:

FRONTEND ↔ BFF

JWT HMAC-SHA256
Claims: sessionId + username
Expiracion: 30 min
ASP.NET Core valida

BFF ↔ DOLIBARR

DOLAPIKEY por sesion
Cacheada en IMemoryCache
Clave: sessionId del JWT
Inyectada como header

✅ La API key de Dolibarr nunca sale del servidor

Flujo de login

  1. Frontend envia credenciales → POST /api/Auth/login
  2. BFF autentica contra Dolibarr y recibe DOLAPIKEY
  3. BFF genera sessionId (GUID) y cachea { sessionId → DOLAPIKEY }
  4. BFF genera JWT con claims { sessionId, username } (30 min)
  5. Frontend usa JWT en Authorization: Bearer para cada request

Cada request: JWT → sessionId → DOLAPIKEY en cache → header inyectado

✓ JWT secreto via variable de entorno (nunca hardcodeado)

Seguridad: Rate Limiting

ASP.NET Core 10 incluye rate limiting nativo. Lo usamos para proteger el login:

builder.Services.AddRateLimiter(options =>
{
    options.RejectionStatusCode = 429;
    options.GlobalLimiter = PartitionedRateLimiter
        .Create<HttpContext, string>(context =>
    {
        if (context.Request.Path.StartsWith("/api/Auth/login"))
            return RateLimitPartition.GetSlidingWindowLimiter(
                "login", _ => new SlidingWindowRateLimiterOptions
                {
                    PermitLimit = 5, Window = TimeSpan.FromMinutes(1)
                });
        return RateLimitPartition.GetNoLimiter("default");
    });
});

Maximo 5 intentos de login por minuto. Exceso → 429 Too Many Requests.

Seguridad: Health Checks + Authorize

HEALTH CHECKS

Endpoint /health que verifica el estado del BFF. Ideal para Docker y Kubernetes.

builder.Services.AddHealthChecks();
app.MapHealthChecks("/health");

AUTHORIZE + SWAGGER DOCS

Todos los controllers con [Authorize]. Todos los endpoints documentados con [ProducesResponseType] — OpenAPI生成 automático.

[ProducesResponseType(typeof(InvoiceDetailDto),
    StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails),
    StatusCodes.Status404NotFound)]

API — Endpoints

MetodoRutaDescripcionAuth
Auth
POST/api/Auth/loginLogin → JWTNo
Clients
GET/api/ClientsListar clientes + contactosJWT
Invoices
GET/api/InvoicesListar (pag., filtros)JWT
GET/api/Invoices/{id}Detalle con lineasJWT
POST/api/InvoicesCrear facturaJWT
PUT/api/Invoices/{id}ActualizarJWT
PATCH/api/Invoices/{id}/statusCambiar estadoJWT
POST/api/Invoices/{id}/validateValidar borradorJWT
DELETE/api/Invoices/{id}Eliminar borradorJWT
POST/api/Invoices/{id}/linesAnadir lineaJWT
DELETE/api/Invoices/{id}/lines/{lineId}Eliminar lineaJWT
GET/api/Invoices/{id}/paymentsListar pagosJWT
POST/api/Invoices/{id}/paymentsRegistrar pagoJWT
Document
GET/api/Document/invoice/{ref}/pdfGenerar PDFJWT
Health
GET/healthEstado del servicioNo

Logica en el BFF

El BFF no es solo un proxy — anade valor:

VALIDACION

[Required], [Range], [StringLength]
Paginacion 1-based con validacion

REGLAS DE NEGOCIO

Solo drafts se pueden eliminar, validar o anadir lineas
Estados limitados: draft/unpaid/paid

TRANSFORMACION

Strings → int/decimal/DateOnly
"1" → "unpaid"
Enriquecer con nombres de cliente

public async Task ValidateInvoiceAsync(int id)
{
    var invoice = await GetRawInvoiceAsync(id);
    if (invoice.Statut != "0")
        throw new ForbiddenException("Solo se pueden validar facturas en borrador");
    await _apiClient.PostAsync($"invoices/{id}/validate", null);
}

CQRS-lite: Command y Query DTOs

Command (entrada)

  • CreateTokenDto
  • CreateInvoiceDto
  • CreateInvoiceLineDto
  • CreateInvoicePaymentDto
  • UpdateInvoiceDto
  • UpdateInvoiceStatusDto

Query (salida)

  • InvoiceDto / InvoiceDetailDto
  • InvoiceLineDto
  • InvoicePaymentDto
  • ClientDto
  • LoginResponse

Tambien Dolibarr DTOs modelan la respuesta cruda del ERP — todo como string.

Testabilidad: IDolibarrApiClient

La comunicacion con Dolibarr se abstrae en una interfaz, permitiendo mocking en tests:

public interface IDolibarrApiClient
{
    Task<T> GetResourceAsync<T>(string endpoint) where T : class;
    Task<List<T>> GetCollectionAsync<T>(string endpoint) where T : class;
    Task<string> PostAsync(string endpoint, object requestBody);
    Task<string> PutAsync(string endpoint, object requestBody);
    Task DeleteAsync(string endpoint);
}

PRODUCCION

DolibarrApiClient — inyecta DOLAPIKEY desde cache y llama al ERP real

TESTS

Mock<IDolibarrApiClient> — tests unitarios sin depender de Dolibarr

Mappers: Antes vs Despues

Dolibarr (crudo)

{
  "statut": "1",
  "total_ttc": "150.50",
  "date": "1715673600",
  "socid": "3",
  "note_public": null
}

BFF (limpio)

{
  "status": "unpaid",
  "total": 150.50,
  "date": "2024-05-14",
  "clientId": 3,
  "notePublic": null
}

Resultado El frontend recibe datos limpios, tipados y en camelCase

Gestion de errores — RFC 7807

// Excepciones de dominio
public class NotFoundException : Exception { }
public class UnauthorizedException : Exception { }
public class ForbiddenException : Exception { }
public class BadRequestException : Exception { }
public class ApiException : Exception { }

// Handler global (Program.cs)
app.UseExceptionHandler(options =>
{
    options.Map<NotFoundException>(ex => Results.Problem(
        statusCode: 404, title: "Recurso no encontrado", detail: ex.Message));
    options.Map<UnauthorizedException>(ex => Results.Problem(
        statusCode: 401, title: "No autorizado", detail: ex.Message));
    // Forbidden -> 403, BadRequest -> 400, Api -> 500
});

Todas las respuestas siguen ProblemDetails RFC 7807: {"type":"...", "title":"...", "status":404}

Pipeline de middleware

  1. ExceptionHandler — ProblemDetails RFC 7807
  2. RateLimiter (🔒 5 req/min en login)
  3. Swagger (solo Development)
  4. HTTPS Redirection
  5. CORS (localhost:3000, 3001, 5173)
  6. Authentication (JWT Bearer)
  7. Authorization
  8. MapControllers
  9. HealthChecks (/health)

Despliegue con Docker

# compose.yaml
services:
  mysql:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: dolibarr
      MYSQL_DATABASE: dolibarr
    volumes: [mysql_data:/var/lib/mysql]

  dolibarr:
    image: dolibarr/dolibarr:latest
    depends_on: [mysql]
    ports: ["80:80"]

  dolimiddlewareapi:
    build: ./DoliMiddlewareApi
    ports: ["5000:8080"]
    depends_on: [dolibarr]

  # redis:  # Cache distribuida (planificado)
  #   image: redis:7-alpine

Dockerfile

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app .
USER $APP_UID              # No-root por seguridad
EXPOSE 8080 8081
ENTRYPOINT ["dotnet", "DoliMiddlewareApi.dll"]

.NET 10 Multi-stage build — runtime ligero, usuario no-root por defecto

Lecciones aprendidas

✓ BFF justificado — el frontend no deberia lidiar con APIs inconsistentes

✓ Doble token — credenciales Dolibarr nunca salen del servidor

✓ Contratos tipados — DTOs con validacion evitan datos incorrectos

✓ Seguridad nativa — Rate limiting, health checks y [Authorize]

✓ Errores centralizados — RFC 7807 ProblemDetails consistentes

✓ Abstraccion para testing — IDolibarrApiClient permite mocking completo

Mejoras pendientes

☐ Tests unitarios (proyecto xUnit + Moq preparado, IDolibarrApiClient facilita mocking)

☐ Redis para cache distribuida

☐ Response caching

☐ Metadatos de paginacion (X-Total-Count)

☐ CI/CD

☐ Mas endpoints: productos, pedidos, terceros...

🙏 Gracias!

DoliMiddlewareApi — BFF para Dolibarr

.NET 10 LTS • JWT • Rate Limiting • Health Checks • Docker

Preguntas →