actualizar presentacion

This commit is contained in:
javiermengual 2026-05-24 13:32:08 +02:00
parent 62bc070115
commit 9f12f582a3
1 changed files with 207 additions and 405 deletions

View File

@ -3,404 +3,251 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DoliMiddlewareApi - BFF para Dolibarr</title>
<title>DoliMiddlewareApi — BFF para Dolibarr</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reveal.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/theme/black.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/plugin/highlight/monokai.css">
<style>
.reveal h1,.reveal h2,.reveal h3,.reveal h4{text-transform:none}
.reveal section{text-align:left}
.reveal h1{text-align:center}
.reveal h2{margin-bottom:10px}
.reveal h1,.reveal h2,.reveal h3{text-transform:none;letter-spacing:0}
.reveal section{padding:22px 46px;box-sizing:border-box}
.reveal h1{font-size:1.7em;font-weight:800;margin-bottom:.1em}
.reveal h2{font-size:1.15em;margin-bottom:.5em;color:#fff;font-weight:700}
.reveal p{font-size:.8em;line-height:1.55;margin:.35em 0}
.reveal ul{font-size:.78em;line-height:1.7;margin:.2em 0 .2em 1.3em}
.reveal pre{width:100%;margin:.5em 0;border-radius:5px}
.reveal pre code{font-size:.62em;line-height:1.25}
body,.reveal{background:#111;color:#eee}
.cols{display:flex;gap:16px;align-items:stretch}
.cols>*{flex:1}
.center{text-align:center}
.img-arch{max-height:55vh;max-width:92%;border-radius:8px;border:2px solid #42affa;display:block;margin:0 auto}
.two-col{display:flex;gap:18px}
.two-col>div{flex:1}
.m{background:#42affa;color:#111;border-radius:3px;padding:1px 6px;font-family:monospace;font-size:.68em;font-weight:bold}
.m-get{background:#61affe}.m-post{background:#49cc90;color:#111}.m-put{background:#fca130;color:#111}.m-patch{background:#50e3c2;color:#111}.m-del{background:#f93e3e;color:#fff}
.pill{display:inline-block;background:#222;border:1px solid #555;border-radius:12px;padding:2px 10px;font-size:.68em;margin:2px}
.pill-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:middle}
.ep{border-collapse:collapse;width:100%}
.ep td,.ep th{padding:2px 5px;border-bottom:1px solid #333}
.ep th{color:#42affa;border-bottom:2px solid #42affa}
.card{background:#1a1a2e;border:1px solid #42affa44;border-radius:8px;padding:8px 12px;margin:5px 0}
.pro{color:#49cc90}.con{color:#f93e3e}.warn{color:#fca130}
.reveal pre{width:100%}
.reveal code{font-size:.72em}
.reveal pre code{font-size:.63em;line-height:1.22}
.section-label{display:inline-block;background:#42affa22;border:1px solid #42affa55;border-radius:4px;padding:1px 8px;font-size:.6em;color:#42affa;margin-bottom:5px}
.card{
background:#181818;
border:1px solid #333;
border-radius:6px;
padding:16px 20px;
margin:5px 0;
}
.callout{
background:#1c1a12;
border:1px solid #e2a84b44;
padding:14px 20px;
border-radius:6px;
margin:.7em 0;
}
.callout p{margin:0;font-size:.78em;line-height:1.5}
.badge{
display:inline-block;
background:#181818;
border:1px solid #444;
border-radius:3px;
padding:5px 12px;
font-size:.58em;
margin:2px;
color:#aaa;
font-family:'SF Mono',Consolas,monospace;
}
.flow{display:flex;align-items:center;justify-content:center;gap:16px;margin:1em 0;font-size:.85em}
.flow-box{padding:14px 26px;border-radius:6px;font-weight:700;text-align:center;font-size:.95em}
.flow-arrow{color:#e2a84b;font-size:1.5em;font-weight:800}
.img-full{width:100%;max-height:52vh;object-fit:contain;border-radius:6px;display:block;margin:0 auto}
.ep{display:grid;grid-template-columns:1fr 1fr;gap:6px 20px;margin-top:.4em}
.ep p{margin:0;line-height:1.65}
.sub{font-size:.68em;color:#888;line-height:1.5}
.glow{font-size:.95em;color:#e2a84b;font-weight:700}
.big{font-size:1.5em;font-weight:800}
</style>
</head>
<body>
<div class="reveal"><div class="slides">
<section style="text-align:center">
<h1 style="font-size:1.9em">DoliMiddlewareApi</h1>
<p style="font-size:1em;color:#42affa">Backend for Frontend para Dolibarr ERP</p>
<p style="font-size:.75em;color:#666;margin-top:30px">Trabajo Fin de Grado &mdash; JavierMB &mdash; 2026</p>
<div style="margin-top:20px">
<span class="pill"><span class="pill-dot" style="background:#512bd4"></span>.NET 10 LTS</span>
<span class="pill"><span class="pill-dot" style="background:#49cc90"></span>ASP.NET Core</span>
<span class="pill"><span class="pill-dot" style="background:#fca130"></span>Docker</span>
<span class="pill"><span class="pill-dot" style="background:#61affe"></span>JWT Auth</span>
<!-- PORTADA -->
<section class="center">
<p style="font-size:.65em;color:#888;margin-bottom:.5em">TFG — Javier Mengual — 2026</p>
<h1 style="color:#fff">DoliMiddlewareApi</h1>
<p style="font-size:.95em;color:#e2a84b;margin:.2em 0 1.8em">Tu frontend merece algo mejor que strings rotos</p>
</section>
<!-- EL PROBLEMA -->
<section>
<h2>El frontend no debería tener que lidiar con esto</h2>
<p>Dolibarr funciona. Pero su API devuelve un desastre que no puedes entregarle a un frontend moderno y decir "aquí tienes, apañatelas".</p>
<div class="cols" style="margin-top:.8em">
<div class="card" style="border-top:3px solid #f87171">
<p style="font-size:.8em;color:#f87171;font-weight:700;margin:0 0 8px">Lo que te da Dolibarr</p>
<div class="sub" style="line-height:1.85">
IDs como <code>"string"</code><br>
Fechas en timestamp Unix<br>
Estados que son números: <code>"1"</code><br>
snake_case inconsistente<br>
Credenciales volando por ahí
</div>
</div>
<div class="card" style="border-top:3px solid #4ade80">
<p style="font-size:.8em;color:#4ade80;font-weight:700;margin:0 0 8px">Lo que tu frontend merece</p>
<div class="sub" style="line-height:1.85">
IDs como <code>int</code><br>
Fechas ISO 8601<br>
Estados legibles: <code>"unpaid"</code><br>
camelCase<br>
Un JWT y ya está
</div>
</div>
</div>
</section>
<!-- BFF -->
<section>
<h2>Agenda</h2>
<ol style="font-size:.72em;line-height:2;columns:2">
<li>El problema</li><li>Patron BFF y arquitectura</li>
<li>.NET 10 y por que LTS</li><li>Stack tecnologico</li>
<li>Autenticacion dual</li><li>API y endpoints</li>
<li>DTOs, Mappers y CQRS-lite</li><li>Testabilidad: IDolibarrApiClient</li>
<li>Seguridad: Rate Limiting y mas</li><li>Errores RFC 7807</li>
<li>Docker y despliegue</li><li>Lecciones y futuro</li>
</ol>
</section>
<section>
<h2>El problema</h2>
<div class="card" style="padding:8px 12px">
<p class="con" style="font-size:.7em;line-height:1.5;margin:0">
&#x274C; Dolibarr devuelve <strong>todo como string</strong> (ids, fechas, decimales, estados)<br>
&#x274C; API REST inconsistente (snake_case, unix timestamps, codigos numericos)<br>
&#x274C; Exponer el ERP directamente al navegador = riesgo de seguridad<br>
&#x274C; El frontend tendria que parsear y transformar todo manualmente
</p>
<h2>Un escudo entre tu app y el caos</h2>
<p>El patrón BFF —Backend for Frontend— no es un proxy. Es una capa que traduce, protege y enriquece. Tu frontend habla con él, y él se ocupa de lo demás.</p>
<div class="flow" style="margin:1.3em 0">
<div class="flow-box" style="background:#1e1510;color:#fb923c;border:2px solid #fb923c88">Dolibarr</div>
<span class="flow-arrow">⟶</span>
<div class="flow-box" style="background:#1a1710;color:#e2a84b;border:2px solid #e2a84b88">BFF</div>
<span class="flow-arrow">⟶</span>
<div class="flow-box" style="background:#0f1a11;color:#4ade80;border:2px solid #4ade8088">Tu frontend</div>
</div>
<div class="card" style="padding:8px 12px;border-color:#49cc9044">
<p class="pro" style="font-size:.7em;line-height:1.5;margin:0">
&#x2705; El BFF <strong>transforma, valida y cachea</strong> &mdash; frontend recibe datos limpios<br>
&#x2705; JWT propio &mdash; credenciales Dolibarr <strong>nunca</strong> llegan al navegador<br>
&#x2705; Contratos tipados con DataAnnotations y mappers
</p>
</div>
</section>
<section>
<h2>Patron BFF y arquitectura</h2>
<p style="font-size:.75em">Un BFF es una API disenada <strong>exclusivamente</strong> para un tipo de cliente frontend.</p>
<div style="margin-top:12px;text-align:center;font-size:.85em">
<div style="display:inline-block;text-align:center;vertical-align:middle">
<div style="background:#f7df1e;color:#111;padding:10px 22px;border-radius:8px;font-weight:bold">JS Frontend<br><small>JavaScript</small></div>
</div>
<span style="font-size:1.4em;vertical-align:middle;margin:0 8px">&harr;</span>
<div style="display:inline-block;text-align:center;vertical-align:middle">
<div style="background:#512bd4;color:#fff;padding:10px 22px;border-radius:8px;font-weight:bold">BFF<br><small>.NET 10</small></div>
</div>
<span style="font-size:1.4em;vertical-align:middle;margin:0 8px">&harr;</span>
<div style="display:inline-block;text-align:center;vertical-align:middle">
<div style="background:#e8650d;color:#fff;padding:10px 22px;border-radius:8px;font-weight:bold">Dolibarr<br><small>ERP</small></div>
</div>
</div>
<ul style="font-size:.68em;line-height:1.6;margin-top:10px">
<li>Abstrae la API legacy detras de endpoints limpios y RESTful</li>
<li>Transforma tipos (strings a int/decimal/DateOnly, estados numericos a texto)</li>
<li>Aplica reglas de negocio y validacion</li>
<li>Centraliza autenticacion: el frontend solo maneja JWT</li>
<ul>
<li>El frontend nunca habla con Dolibarr directamente</li>
<li>Si el ERP cambia algo, tocas un sitio, no cincuenta</li>
<li>Y puedes meterle cosas que Dolibarr jamás va a tener</li>
</ul>
</section>
<section>
<h2>Arquitectura general</h2>
<img src="arquitectura.png" class="img-arch" alt="Arquitectura">
<!-- ARQUITECTURA -->
<section class="center">
<h2>Así funciona</h2>
<img src="arquitectura.png" class="img-full" alt="Arquitectura" style="max-height:54vh">
</section>
<section>
<h2>.NET 10 LTS &mdash; por que esta eleccion</h2>
<p style="font-size:.72em">Elegimos <strong>.NET 10 LTS</strong> (soporte hasta noviembre 2028). Ventajas clave:</p>
<div style="display:flex;gap:8px;margin-top:8px">
<div class="card" style="flex:1;padding:8px 10px">
<p style="font-size:.65em;color:#42affa;font-weight:bold;margin:0 0 3px">PERFORMANCE</p>
<p style="font-size:.63em;margin:0">JIT inlining, escape analysis, NativeAOT mas rapido y ligero</p>
<!-- .NET 10 -->
<section data-background-image="dotnetall.jpg" data-background-size="cover" data-background-position="center" data-background-opacity="0.12">
<h2>¿Por qué .NET 10?</h2>
<p>Si voy a poner una API en producción, quiero dormir tranquilo tres años.</p>
<img src="dotnetall.jpg" style="width:100%;max-height:32vh;object-fit:cover;object-position:center;border-radius:6px;display:block;margin-bottom:10px">
<div class="cols" style="gap:10px">
<div class="card" style="border-top:3px solid #e2a84b">
<p style="font-size:.76em;color:#e2a84b;font-weight:700;margin:0 0 3px">LTS hasta 2028</p>
<p class="sub">Tres años de soporte. Me da igual lo que pase meantime.</p>
</div>
<div class="card" style="flex:1;padding:8px 10px">
<p style="font-size:.65em;color:#42affa;font-weight:bold;margin:0 0 3px">C# 14</p>
<p style="font-size:.63em;margin:0">Field-backed properties, extension types, <code>?=</code></p>
<div class="card" style="border-top:3px solid #e2a84b">
<p style="font-size:.76em;color:#e2a84b;font-weight:700;margin:0 0 3px">Rendimiento</p>
<p class="sub">Lo suficientemente rápido para que la latencia no sea un problema.</p>
</div>
<div class="card" style="flex:1;padding:8px 10px">
<p style="font-size:.65em;color:#42affa;font-weight:bold;margin:0 0 3px">ASP.NET CORE 10</p>
<p style="font-size:.63em;margin:0">OpenAPI 3.1 por defecto, validacion built-in, rate limiting nativo</p>
</div>
</div>
<p style="font-size:.65em;margin-top:8px;color:#888">LTS = Long Term Support &mdash; parches de seguridad y soporte durante 3 anos. La eleccion correcta para un TFG.</p>
</section>
<section>
<h2>Stack tecnologico</h2>
<div style="display:flex;gap:10px">
<div class="card" style="flex:1;padding:8px 10px">
<p style="font-size:.72em;color:#42affa;font-weight:bold;margin:0 0 4px">Backend</p>
<p style="font-size:.66em;line-height:1.6;margin:0">.NET 10 LTS<br>ASP.NET Core Web API<br>JWT Bearer Auth<br>Rate Limiting nativo<br>Health Checks</p>
</div>
<div class="card" style="flex:1;padding:8px 10px">
<p style="font-size:.72em;color:#42affa;font-weight:bold;margin:0 0 4px">Patrones</p>
<p style="font-size:.66em;line-height:1.6;margin:0">CQRS-lite (command/query)<br>Service Layer<br>Static Mappers<br>ProblemDetails (RFC 7807)<br>IMemoryCache<br>Interface segregation</p>
</div>
<div class="card" style="flex:1;padding:8px 10px">
<p style="font-size:.72em;color:#42affa;font-weight:bold;margin:0 0 4px">Infra</p>
<p style="font-size:.66em;line-height:1.6;margin:0">Docker + Compose<br>MySQL 8.0<br>Dolibarr (container)<br>xUnit + Moq<br>Swagger / OpenAPI</p>
<div class="card" style="border-top:3px solid #e2a84b">
<p style="font-size:.76em;color:#e2a84b;font-weight:700;margin:0 0 3px">Todo incluido</p>
<p class="sub">JWT, rate limiting, health checks, OpenAPI. Sin buscar librerías raras.</p>
</div>
</div>
</section>
<!-- ENDPOINTS -->
<section>
<h2>Estructura del proyecto</h2>
<div style="display:flex;gap:8px;font-size:.65em">
<div class="card" style="flex:1;padding:6px 10px">
<p style="color:#61affe;font-weight:bold;margin:0 0 4px">Controllers/</p>
<p style="margin:0;line-height:1.5">AuthController<br>ClientsController<br>InvoicesController<br>DocumentController<br><span style="color:#888" class="pro">&#10003; [ProducesResponseType]</span></p>
</div>
<div class="card" style="flex:1.3;padding:6px 10px">
<p style="color:#49cc90;font-weight:bold;margin:0 0 4px">Services/</p>
<p style="margin:0;line-height:1.5"><span style="color:#888">Auth/</span> AuthService, TokenCache, JwtProvider<br><span style="color:#888">Clients/</span> <span style="color:#49cc90">IDolibarrApiClient</span> interfaz<br>InvoiceService, ClientService, DocumentService</p>
</div>
<div class="card" style="flex:1;padding:6px 10px">
<p style="color:#fca130;font-weight:bold;margin:0 0 4px">Dtos/</p>
<p style="margin:0;line-height:1.5"><span style="color:#fca130">command/</span> CreateInvoice...<br><span style="color:#49cc90">query/</span> InvoiceDto...<br><span style="color:#888">Dolibarr/</span> *Response...</p>
</div>
<div class="card" style="flex:.65;padding:6px 10px">
<p style="color:#c792ea;font-weight:bold;margin:0 0 4px">Otros</p>
<p style="margin:0;line-height:1.5">Mappers/<br>Exceptions/<br>Program.cs</p>
</div>
<h2>Lo que cubre</h2>
<div class="ep">
<p><span style="color:#e2a84b;font-weight:600;font-size:.76em">Auth</span> <span class="sub">— login, JWT</span></p>
<p><span style="color:#e2a84b;font-weight:600;font-size:.76em">Facturas</span> <span class="sub">— CRUD, líneas, pagos, estado</span></p>
<p><span style="color:#e2a84b;font-weight:600;font-size:.76em">Clientes</span> <span class="sub">— con contactos incluidos</span></p>
<p><span style="color:#e2a84b;font-weight:600;font-size:.76em">Proveedor</span> <span class="sub">— facturas de proveedor</span></p>
<p><span style="color:#e2a84b;font-weight:600;font-size:.76em">Bancos</span> <span class="sub">— cuentas y movimientos</span></p>
<p><span style="color:#e2a84b;font-weight:600;font-size:.76em">Documentos</span> <span class="sub">— PDFs de facturas</span></p>
<p><span style="color:#e2a84b;font-weight:600;font-size:.76em">Setup</span> <span class="sub">— diccionarios, país, compañía</span></p>
<p><span style="color:#e2a84b;font-weight:600;font-size:.76em">Notificaciones</span> <span class="sub">— webhooks</span></p>
</div>
<p style="font-size:.6em;margin-top:6px;color:#888"><span class="m" style="background:#49cc90;color:#111">clave</span> <code>IDolibarrApiClient</code> es una interfaz &mdash; facilita mocking y testing</p>
</section>
<section>
<h2>Autenticacion dual</h2>
<p style="font-size:.72em">Dos capas de autenticacion <strong>transparentes</strong> para el frontend:</p>
<div style="display:flex;gap:10px;margin-top:8px">
<div class="card" style="flex:1;padding:8px 10px">
<p style="font-size:.68em;color:#42affa;font-weight:bold;margin:0 0 4px">FRONTEND &harr; BFF</p>
<p style="font-size:.65em;line-height:1.5;margin:0">JWT HMAC-SHA256<br>Claims: sessionId + username<br>Expiracion: 30 min<br>ASP.NET Core valida</p>
</div>
<div class="card" style="flex:1;padding:8px 10px">
<p style="font-size:.68em;color:#fca130;font-weight:bold;margin:0 0 4px">BFF &harr; DOLIBARR</p>
<p style="font-size:.65em;line-height:1.5;margin:0">DOLAPIKEY por sesion<br>Cacheada en IMemoryCache<br>Clave: sessionId del JWT<br>Inyectada como header</p>
</div>
</div>
<p style="font-size:.68em;margin-top:6px" class="pro">&#x2705; La API key de Dolibarr <strong>nunca sale del servidor</strong></p>
</section>
<section>
<h2>Flujo de login</h2>
<ol style="font-size:.72em;line-height:1.8">
<li>Frontend envia credenciales &rarr; <code>POST /api/Auth/login</code></li>
<li>BFF autentica contra Dolibarr y recibe DOLAPIKEY</li>
<li>BFF genera sessionId (GUID) y cachea <code>{ sessionId &rarr; DOLAPIKEY }</code></li>
<li>BFF genera JWT con claims <code>{ sessionId, username }</code> (30 min)</li>
<li>Frontend usa JWT en <code>Authorization: Bearer</code> para cada request</li>
</ol>
<p style="font-size:.68em;margin-top:4px">Cada request: JWT &rarr; sessionId &rarr; DOLAPIKEY en cache &rarr; header inyectado</p>
<p style="font-size:.63em;margin-top:2px;color:#49cc90">&#10003; JWT secreto via variable de entorno (nunca hardcodeado)</p>
</section>
<section>
<h2>Seguridad: Rate Limiting</h2>
<span class="section-label">.NET 10</span>
<p style="font-size:.72em;margin-top:4px">ASP.NET Core 10 incluye <strong>rate limiting nativo</strong>. Lo usamos para proteger el login:</p>
<pre><code class="csharp">builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = 429;
options.GlobalLimiter = PartitionedRateLimiter
.Create&lt;HttpContext, string&gt;(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");
});
});</code></pre>
<p style="font-size:.68em;margin-top:4px">Maximo 5 intentos de login por minuto. Exceso &rarr; <code>429 Too Many Requests</code>.</p>
</section>
<section>
<h2>Seguridad: Health Checks + Authorize</h2>
<span class="section-label">.NET 10</span>
<div style="display:flex;gap:10px;margin-top:6px">
<div class="card" style="flex:1;padding:8px 10px">
<p style="font-size:.68em;color:#42affa;font-weight:bold;margin:0 0 4px">HEALTH CHECKS</p>
<p style="font-size:.65em;margin:0">Endpoint <code>/health</code> que verifica el estado del BFF. Ideal para Docker y Kubernetes.</p>
<pre><code class="csharp" style="font-size:.55em">builder.Services.AddHealthChecks();
app.MapHealthChecks("/health");</code></pre>
</div>
<div class="card" style="flex:1;padding:8px 10px">
<p style="font-size:.68em;color:#42affa;font-weight:bold;margin:0 0 4px">AUTHORIZE + SWAGGER DOCS</p>
<p style="font-size:.65em;margin:0">Todos los controllers con <code>[Authorize]</code>. Todos los endpoints documentados con <code>[ProducesResponseType]</code> &mdash; OpenAPI生成 autom&aacute;tico.</p>
<pre><code class="csharp" style="font-size:.52em">[ProducesResponseType(typeof(InvoiceDetailDto),
StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails),
StatusCodes.Status404NotFound)]</code></pre>
</div>
<div class="callout" style="margin-top:1em">
<p>Sí, gran parte es 1:1 con Dolibarr. Pero el BFF añade lo que el ERP no tiene: <strong>tipos correctos</strong>, <strong>estados legibles</strong>, enriquecimiento con nombres de cliente y notificaciones.</p>
</div>
</section>
<!-- TRANSFORMACIÓN -->
<section>
<h2>API &mdash; Endpoints</h2>
<table class="ep" style="font-size:.56em">
<tr><th>Metodo</th><th>Ruta</th><th>Descripcion</th><th>Auth</th></tr>
<tr><td colspan="4" style="color:#42affa;font-weight:bold;padding-top:3px">Auth</td></tr>
<tr><td><span class="m m-post">POST</span></td><td>/api/Auth/login</td><td>Login &rarr; JWT</td><td class="warn">No</td></tr>
<tr><td colspan="4" style="color:#42affa;font-weight:bold;padding-top:3px">Clients</td></tr>
<tr><td><span class="m m-get">GET</span></td><td>/api/Clients</td><td>Listar clientes + contactos</td><td>JWT</td></tr>
<tr><td colspan="4" style="color:#42affa;font-weight:bold;padding-top:3px">Invoices</td></tr>
<tr><td><span class="m m-get">GET</span></td><td>/api/Invoices</td><td>Listar (pag., filtros)</td><td>JWT</td></tr>
<tr><td><span class="m m-get">GET</span></td><td>/api/Invoices/{id}</td><td>Detalle con lineas</td><td>JWT</td></tr>
<tr><td><span class="m m-post">POST</span></td><td>/api/Invoices</td><td>Crear factura</td><td>JWT</td></tr>
<tr><td><span class="m m-put">PUT</span></td><td>/api/Invoices/{id}</td><td>Actualizar</td><td>JWT</td></tr>
<tr><td><span class="m m-patch">PATCH</span></td><td>/api/Invoices/{id}/status</td><td>Cambiar estado</td><td>JWT</td></tr>
<tr><td><span class="m m-post">POST</span></td><td>/api/Invoices/{id}/validate</td><td>Validar borrador</td><td>JWT</td></tr>
<tr><td><span class="m m-del">DELETE</span></td><td>/api/Invoices/{id}</td><td>Eliminar borrador</td><td>JWT</td></tr>
<tr><td><span class="m m-post">POST</span></td><td>/api/Invoices/{id}/lines</td><td>Anadir linea</td><td>JWT</td></tr>
<tr><td><span class="m m-del">DELETE</span></td><td>/api/Invoices/{id}/lines/{lineId}</td><td>Eliminar linea</td><td>JWT</td></tr>
<tr><td><span class="m m-get">GET</span></td><td>/api/Invoices/{id}/payments</td><td>Listar pagos</td><td>JWT</td></tr>
<tr><td><span class="m m-post">POST</span></td><td>/api/Invoices/{id}/payments</td><td>Registrar pago</td><td>JWT</td></tr>
<tr><td colspan="4" style="color:#42affa;font-weight:bold;padding-top:3px">Document</td></tr>
<tr><td><span class="m m-get">GET</span></td><td>/api/Document/invoice/{ref}/pdf</td><td>Generar PDF</td><td>JWT</td></tr>
<tr><td colspan="4" style="color:#42affa;font-weight:bold;padding-top:3px">Health</td></tr>
<tr><td><span class="m m-get">GET</span></td><td>/health</td><td>Estado del servicio</td><td class="warn">No</td></tr>
</table>
</section>
<section>
<h2>Logica en el BFF</h2>
<p style="font-size:.72em">El BFF <strong>no es solo un proxy</strong> &mdash; anade valor:</p>
<div style="display:flex;gap:8px;margin-top:6px">
<div class="card" style="flex:1;padding:6px 10px">
<p style="font-size:.65em;color:#42affa;font-weight:bold;margin:0 0 3px">VALIDACION</p>
<p style="font-size:.6em;margin:0"><code>[Required]</code>, <code>[Range]</code>, <code>[StringLength]</code><br>Paginacion 1-based con validacion</p>
</div>
<div class="card" style="flex:1;padding:6px 10px">
<p style="font-size:.65em;color:#42affa;font-weight:bold;margin:0 0 3px">REGLAS DE NEGOCIO</p>
<p style="font-size:.6em;margin:0">Solo drafts se pueden eliminar, validar o anadir lineas<br>Estados limitados: draft/unpaid/paid</p>
</div>
<div class="card" style="flex:1;padding:6px 10px">
<p style="font-size:.65em;color:#42affa;font-weight:bold;margin:0 0 3px">TRANSFORMACION</p>
<p style="font-size:.6em;margin:0">Strings &rarr; int/decimal/DateOnly<br><code>"1"</code> &rarr; <code>"unpaid"</code><br>Enriquecer con nombres de cliente</p>
</div>
</div>
<pre><code class="csharp" style="font-size:.55em;margin-top:6px">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);
}</code></pre>
</section>
<section>
<h2>CQRS-lite: Command y Query DTOs</h2>
<div class="two-col" style="font-size:.7em;line-height:1.7">
<h2>De francés y strings... a inglés y tipos</h2>
<div class="cols" style="align-items:flex-start;margin-top:.2em">
<div>
<p style="color:#fca130;font-weight:bold">Command (entrada)</p>
<ul><li>CreateTokenDto</li><li>CreateInvoiceDto</li><li>CreateInvoiceLineDto</li><li>CreateInvoicePaymentDto</li><li>UpdateInvoiceDto</li><li>UpdateInvoiceStatusDto</li></ul>
</div>
<div>
<p style="color:#49cc90;font-weight:bold">Query (salida)</p>
<ul><li>InvoiceDto / InvoiceDetailDto</li><li>InvoiceLineDto</li><li>InvoicePaymentDto</li><li>ClientDto</li><li>LoginResponse</li></ul>
</div>
</div>
<p style="font-size:.68em;margin-top:6px">Tambien <strong>Dolibarr DTOs</strong> modelan la respuesta cruda del ERP &mdash; todo como string.</p>
</section>
<section>
<h2>Testabilidad: IDolibarrApiClient</h2>
<p style="font-size:.72em">La comunicacion con Dolibarr se abstrae en una <strong>interfaz</strong>, permitiendo mocking en tests:</p>
<pre><code class="csharp" style="font-size:.63em">public interface IDolibarrApiClient
{
Task&lt;T&gt; GetResourceAsync&lt;T&gt;(string endpoint) where T : class;
Task&lt;List&lt;T&gt;&gt; GetCollectionAsync&lt;T&gt;(string endpoint) where T : class;
Task&lt;string&gt; PostAsync(string endpoint, object requestBody);
Task&lt;string&gt; PutAsync(string endpoint, object requestBody);
Task DeleteAsync(string endpoint);
}</code></pre>
<div style="display:flex;gap:8px;margin-top:6px;font-size:.65em">
<div class="card" style="flex:1;padding:6px 10px">
<p class="pro" style="font-weight:bold;margin:0 0 3px">PRODUCCION</p>
<p style="margin:0"><code>DolibarrApiClient</code> &mdash; inyecta DOLAPIKEY desde cache y llama al ERP real</p>
</div>
<div class="card" style="flex:1;padding:6px 10px">
<p style="font-weight:bold;margin:0 0 3px;color:#fca130">TESTS</p>
<p style="margin:0"><code>Mock&lt;IDolibarrApiClient&gt;</code> &mdash; tests unitarios sin depender de Dolibarr</p>
</div>
</div>
</section>
<section>
<h2>Mappers: Antes vs Despues</h2>
<div class="two-col">
<div>
<p style="color:#f93e3e;font-weight:bold;font-size:.72em">Dolibarr (crudo)</p>
<pre><code class="json" style="font-size:.65em">{
"statut": "1",
<p class="red" style="font-size:.76em;font-weight:700;margin-bottom:5px">Dolibarr</p>
<pre><code class="json" style="font-size:.62em">{
"statut": "1",
"total_ttc": "150.50",
"date": "1715673600",
"socid": "3",
"note_public": null
"date": "1715673600",
"socid": "3"
}</code></pre>
</div>
<div>
<p style="color:#49cc90;font-weight:bold;font-size:.72em">BFF (limpio)</p>
<pre><code class="json" style="font-size:.65em">{
"status": "unpaid",
"total": 150.50,
"date": "2024-05-14",
"clientId": 3,
"notePublic": null
<p class="green" style="font-size:.76em;font-weight:700;margin-bottom:5px">BFF</p>
<pre><code class="json" style="font-size:.62em">{
"status": "unpaid",
"total": 150.50,
"date": "2024-05-14",
"clientId": 3
}</code></pre>
</div>
</div>
<p style="font-size:.68em;margin-top:6px;text-align:center"><span class="m" style="background:#49cc90;color:#111">Resultado</span> El frontend recibe datos limpios, tipados y en camelCase</p>
<p style="font-size:.78em;margin-top:.8em;color:#ccc">Mismo dato. Otro mundo.</p>
</section>
<section>
<h2>Gestion de errores &mdash; RFC 7807</h2>
<pre><code class="csharp">// 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&lt;NotFoundException&gt;(ex => Results.Problem(
statusCode: 404, title: "Recurso no encontrado", detail: ex.Message));
options.Map&lt;UnauthorizedException&gt;(ex => Results.Problem(
statusCode: 401, title: "No autorizado", detail: ex.Message));
// Forbidden -> 403, BadRequest -> 400, Api -> 500
});</code></pre>
<p style="font-size:.68em;margin-top:4px">Todas las respuestas siguen ProblemDetails RFC 7807: <code>{"type":"...", "title":"...", "status":404}</code></p>
<!-- SWAGGER -->
<section class="center">
<h2>Documentación automática</h2>
<img src="swagger.png" class="img-full" alt="Swagger UI" style="max-height:50vh">
<p class="sub" style="margin-top:.5em">OpenAPI 3.1 generado al arrancar. Cada endpoint documentado.</p>
</section>
<!-- AUTENTICACIÓN -->
<section>
<h2>Pipeline de middleware</h2>
<ol style="font-size:.72em;line-height:1.9">
<li><strong>ExceptionHandler</strong> &mdash; ProblemDetails RFC 7807</li>
<li><strong>RateLimiter</strong> <span style="color:#888">(&#x1F512; 5 req/min en login)</span></li>
<li><strong>Swagger</strong> <span style="color:#888">(solo Development)</span></li>
<li><strong>HTTPS Redirection</strong></li>
<li><strong>CORS</strong> <span style="color:#888">(localhost:3000, 3001, 5173)</span></li>
<li><strong>Authentication</strong> <span style="color:#888">(JWT Bearer)</span></li>
<li><strong>Authorization</strong></li>
<li><strong>MapControllers</strong></li>
<li><strong>HealthChecks</strong> <span style="color:#888">(/health)</span></li>
</ol>
<h2>La contraseña nunca sale del servidor</h2>
<p>El frontend maneja un JWT. La API key de Dolibarr queda atrapada dentro del BFF. Si alguien intercepta el token del usuario, caduca en 8 horas y no tiene acceso al ERP.</p>
<div class="cols" style="margin-top:.8em">
<div class="card" style="border-top:3px solid #e2a84b">
<p style="font-size:.78em;color:#e2a84b;font-weight:700;margin:0 0 4px">1 — Login</p>
<p class="sub">El usuario se autentica. El BFF recibe la API key de Dolibarr y la guarda en memoria.</p>
</div>
<div class="card" style="border-top:3px solid #4ade80">
<p style="font-size:.78em;color:#4ade80;font-weight:700;margin:0 0 4px">2 — JWT</p>
<p class="sub">El BFF genera un JWT para el navegador. La API key nunca sale de ahí.</p>
</div>
<div class="card" style="border-top:3px solid #fb923c">
<p style="font-size:.78em;color:#fb923c;font-weight:700;margin:0 0 4px">3 — Cada petición</p>
<p class="sub">El JWT lleva un sessionId. Con ese sessionId se busca la API key en caché y se inyecta al vuelo.</p>
</div>
</div>
<div class="callout" style="margin-top:.7em">
<p>Cada usuario tiene su propia API key aislada. Si se cachease en el HttpClient, todos compartirían la misma.</p>
</div>
</section>
<!-- NOTIFICACIONES -->
<section>
<h2>Despliegue con Docker</h2>
<pre><code class="yaml" style="font-size:.65em"># compose.yaml
services:
<h2>Lo que un wrapper puede hacer que el ERP no</h2>
<p>Al poner una capa por delante, puedes añadirle cosas que Dolibarr nunca va a tener. El ejemplo más claro: cuando una factura cambia de estado, el equipo se entera al momento. Sin abrir el sistema, sin mirar nada.</p>
<div class="cols" style="margin-top:.7em">
<div class="card" style="border-top:3px solid #c084fc">
<p style="font-size:.78em;color:#c084fc;font-weight:700;margin:0 0 4px">Teams, Slack, lo que sea</p>
<p class="sub">Configuras una URL y avisa donde quieras. Hoy es Teams, mañana puede ser Telegram, email, lo que haga falta.</p>
</div>
<div class="card" style="border-top:3px solid #4ade80">
<p style="font-size:.78em;color:#4ade80;font-weight:700;margin:0 0 4px">La interfaz ya está</p>
<p class="sub">Añadir un canal nuevo es implementar una interfaz. El resto del código ni se toca. Así se escala un wrapper de verdad.</p>
</div>
</div>
</section>
<!-- DESPLIEGUE -->
<section>
<h2>Un comando y funciona</h2>
<p class="sub" style="margin-bottom:.3em">docker compose up</p>
<pre><code class="yaml" style="font-size:.64em">services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: dolibarr
MYSQL_DATABASE: dolibarr
volumes: [mysql_data:/var/lib/mysql]
@ -409,62 +256,16 @@ services:
depends_on: [mysql]
ports: ["80:80"]
dolimiddlewareapi:
bff:
build: ./DoliMiddlewareApi
ports: ["5000:8080"]
depends_on: [dolibarr]
# redis: # Cache distribuida (planificado)
# image: redis:7-alpine</code></pre>
ports: ["5001:8080"]</code></pre>
<p style="font-size:.78em;margin-top:.7em;color:#bbb">MySQL, Dolibarr, el BFF. Tres contenedores, un comando.</p>
</section>
<section>
<h2>Dockerfile</h2>
<pre><code class="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"]</code></pre>
<p style="font-size:.68em;margin-top:6px"><span class="m" style="background:#512bd4;color:#fff">.NET 10</span> Multi-stage build &mdash; runtime ligero, usuario no-root por defecto</p>
</section>
<section>
<h2>Lecciones aprendidas</h2>
<div style="font-size:.72em;line-height:2">
<p class="pro">&#10003; <strong>BFF justificado</strong> &mdash; el frontend no deberia lidiar con APIs inconsistentes</p>
<p class="pro">&#10003; <strong>Doble token</strong> &mdash; credenciales Dolibarr nunca salen del servidor</p>
<p class="pro">&#10003; <strong>Contratos tipados</strong> &mdash; DTOs con validacion evitan datos incorrectos</p>
<p class="pro">&#10003; <strong>Seguridad nativa</strong> &mdash; Rate limiting, health checks y [Authorize]</p>
<p class="pro">&#10003; <strong>Errores centralizados</strong> &mdash; RFC 7807 ProblemDetails consistentes</p>
<p class="pro">&#10003; <strong>Abstraccion para testing</strong> &mdash; IDolibarrApiClient permite mocking completo</p>
</div>
</section>
<section>
<h2>Mejoras pendientes</h2>
<div style="font-size:.72em;line-height:2">
<p>&#9744; Tests unitarios (proyecto xUnit + Moq preparado, <code>IDolibarrApiClient</code> facilita mocking)</p>
<p>&#9744; Redis para cache distribuida</p>
<p>&#9744; Response caching</p>
<p>&#9744; Metadatos de paginacion (X-Total-Count)</p>
<p>&#9744; CI/CD</p>
<p>&#9744; Mas endpoints: productos, pedidos, terceros...</p>
</div>
</section>
<section style="text-align:center">
<h1>&#x1F64F; Gracias!</h1>
<p style="font-size:.9em;margin-top:15px">DoliMiddlewareApi &mdash; BFF para Dolibarr</p>
<p style="font-size:.7em;color:#888;margin-top:10px">.NET 10 LTS &bull; JWT &bull; Rate Limiting &bull; Health Checks &bull; Docker</p>
<p style="font-size:.75em;color:#42affa;margin-top:25px">Preguntas &rarr;</p>
<!-- CIERRE -->
<section class="center">
<h1 style="font-size:2.2em">Preguntas</h1>
</section>
</div></div>
@ -474,13 +275,14 @@ ENTRYPOINT ["dotnet", "DoliMiddlewareApi.dll"]</code></pre>
<script>
Reveal.initialize({
hash: true,
slideNumber: true,
transition: 'slide',
transitionSpeed: 'default',
plugins: [ RevealHighlight ],
width: 1200,
height: 700,
slideNumber: 'c/t',
transition: 'fade',
transitionSpeed: 'fast',
plugins: [RevealHighlight],
width: 1100,
height: 680,
margin: 0.04,
center: false,
});
</script>
</body>