487 lines
26 KiB
HTML
487 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<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}
|
|
.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}
|
|
</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>
|
|
</div>
|
|
</section>
|
|
|
|
<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>
|
|
</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>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Arquitectura general</h2>
|
|
<img src="arquitectura.png" class="img-arch" alt="Arquitectura">
|
|
</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>
|
|
</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>
|
|
<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>
|
|
</div>
|
|
</section>
|
|
|
|
<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>
|
|
</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>
|
|
</section>
|
|
|
|
<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">
|
|
<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",
|
|
"total_ttc": "150.50",
|
|
"date": "1715673600",
|
|
"socid": "3",
|
|
"note_public": null
|
|
}</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
|
|
}</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>
|
|
</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>
|
|
</section>
|
|
|
|
<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>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Despliegue con Docker</h2>
|
|
<pre><code class="yaml" style="font-size:.65em"># 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</code></pre>
|
|
</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>
|
|
</section>
|
|
|
|
</div></div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/dist/reveal.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/reveal.js@5.1.0/plugin/highlight/highlight.js"></script>
|
|
<script>
|
|
Reveal.initialize({
|
|
hash: true,
|
|
slideNumber: true,
|
|
transition: 'slide',
|
|
transitionSpeed: 'default',
|
|
plugins: [ RevealHighlight ],
|
|
width: 1200,
|
|
height: 700,
|
|
margin: 0.04,
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |