actualizar presentacion
This commit is contained in:
parent
62bc070115
commit
9f12f582a3
|
|
@ -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 — JavierMB — 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">
|
||||
❌ Dolibarr devuelve <strong>todo como string</strong> (ids, fechas, decimales, estados)<br>
|
||||
❌ API REST inconsistente (snake_case, unix timestamps, codigos numericos)<br>
|
||||
❌ Exponer el ERP directamente al navegador = riesgo de seguridad<br>
|
||||
❌ 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">
|
||||
✅ El BFF <strong>transforma, valida y cachea</strong> — frontend recibe datos limpios<br>
|
||||
✅ JWT propio — credenciales Dolibarr <strong>nunca</strong> llegan al navegador<br>
|
||||
✅ 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">↔</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">↔</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 — 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 — 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">✓ [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 — 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 ↔ 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 ↔ 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">✅ 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 → <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 → 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 → sessionId → DOLAPIKEY en cache → header inyectado</p>
|
||||
<p style="font-size:.63em;margin-top:2px;color:#49cc90">✓ 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<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");
|
||||
});
|
||||
});</code></pre>
|
||||
<p style="font-size:.68em;margin-top:4px">Maximo 5 intentos de login por minuto. Exceso → <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> — OpenAPI生成 automá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 — 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 → 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> — 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 → int/decimal/DateOnly<br><code>"1"</code> → <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 — 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<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);
|
||||
}</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> — 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<IDolibarrApiClient></code> — 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 — 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<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
|
||||
});</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> — ProblemDetails RFC 7807</li>
|
||||
<li><strong>RateLimiter</strong> <span style="color:#888">(🔒 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 — 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">✓ <strong>BFF justificado</strong> — el frontend no deberia lidiar con APIs inconsistentes</p>
|
||||
<p class="pro">✓ <strong>Doble token</strong> — credenciales Dolibarr nunca salen del servidor</p>
|
||||
<p class="pro">✓ <strong>Contratos tipados</strong> — DTOs con validacion evitan datos incorrectos</p>
|
||||
<p class="pro">✓ <strong>Seguridad nativa</strong> — Rate limiting, health checks y [Authorize]</p>
|
||||
<p class="pro">✓ <strong>Errores centralizados</strong> — RFC 7807 ProblemDetails consistentes</p>
|
||||
<p class="pro">✓ <strong>Abstraccion para testing</strong> — IDolibarrApiClient permite mocking completo</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Mejoras pendientes</h2>
|
||||
<div style="font-size:.72em;line-height:2">
|
||||
<p>☐ Tests unitarios (proyecto xUnit + Moq preparado, <code>IDolibarrApiClient</code> facilita mocking)</p>
|
||||
<p>☐ Redis para cache distribuida</p>
|
||||
<p>☐ Response caching</p>
|
||||
<p>☐ Metadatos de paginacion (X-Total-Count)</p>
|
||||
<p>☐ CI/CD</p>
|
||||
<p>☐ Mas endpoints: productos, pedidos, terceros...</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section style="text-align:center">
|
||||
<h1>🙏 Gracias!</h1>
|
||||
<p style="font-size:.9em;margin-top:15px">DoliMiddlewareApi — BFF para Dolibarr</p>
|
||||
<p style="font-size:.7em;color:#888;margin-top:10px">.NET 10 LTS • JWT • Rate Limiting • Health Checks • Docker</p>
|
||||
<p style="font-size:.75em;color:#42affa;margin-top:25px">Preguntas →</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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue