From fe6bd57796967a6bf013e4d286fb6656bb099526 Mon Sep 17 00:00:00 2001 From: JavMB Date: Fri, 15 May 2026 18:58:43 +0200 Subject: [PATCH] feat: configure Swagger Bearer auth, fix CORS, Docker and deployment - Program.cs: added Swagger with JWT Bearer security definition using OpenApi v2 API (OpenApiSecuritySchemeReference), enabled Swagger in all environments (not just Development), registered all new services (ContactService, SetupService) in DI - CORS: configurable via Cors__AllowedOrigins env var, allows all when empty (dev-friendly) - DoliMiddlewareApi.csproj: removed Microsoft.AspNetCore.OpenApi (conflicts with Swashbuckle v10), kept Swashbuckle.AspNetCore 10.1.0 - appsettings.json: fixed Dolibarr ApiUrl to /api/index.php (was /explorer) - compose.yaml: fixed Dolibarr ApiUrl, added JWT_SECRET default for dev, added healthcheck for Dolibarr container, BFF waits for Dolibarr healthy - .dockerignore: added to keep Docker build clean - .env.example: template with instructions for JWT_SECRET --- .dockerignore | 41 +++---- .env.example | 16 +++ DoliMiddlewareApi/DoliMiddlewareApi.csproj | 1 - DoliMiddlewareApi/Program.cs | 114 ++++++++++++++---- .../appsettings.Development.json | 3 + DoliMiddlewareApi/appsettings.json | 7 +- compose.yaml | 67 +++++----- 7 files changed, 162 insertions(+), 87 deletions(-) create mode 100644 .env.example diff --git a/.dockerignore b/.dockerignore index 38bece4..2a7a8be 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,25 +1,16 @@ -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/.idea -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md \ No newline at end of file +bin/ +obj/ +.vs/ +.vscode/ +node_modules/ +*.user +*.suo +*.log +.env +.env.local +.git/ +.gitignore +docker-compose*.yml +*.md +presentacion/ +DoliMiddlewareApi.Tests/ \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f07ef0c --- /dev/null +++ b/.env.example @@ -0,0 +1,16 @@ +# ============================================ +# DoliMiddlewareApi - Environment Variables +# ============================================ +# Copy this file to .env and fill in the values +# NEVER commit .env with real secrets to git + +# JWT Secret: must be at least 32 characters +# Generate one with: openssl rand -base64 48 +JWT_SECRET=CHANGE-ME-TO-A-LONG-RANDOM-STRING-AT-LEAST-32-CHARS + +# MySQL +MYSQL_ROOT_PASSWORD=rootpassword +MYSQL_PASSWORD=12345678 + +# Dolibarr Admin +DOLI_ADMIN_PASSWORD=12345678 \ No newline at end of file diff --git a/DoliMiddlewareApi/DoliMiddlewareApi.csproj b/DoliMiddlewareApi/DoliMiddlewareApi.csproj index 08593a2..feaf44e 100644 --- a/DoliMiddlewareApi/DoliMiddlewareApi.csproj +++ b/DoliMiddlewareApi/DoliMiddlewareApi.csproj @@ -10,7 +10,6 @@ - diff --git a/DoliMiddlewareApi/Program.cs b/DoliMiddlewareApi/Program.cs index ba25023..d9d983d 100644 --- a/DoliMiddlewareApi/Program.cs +++ b/DoliMiddlewareApi/Program.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Threading.RateLimiting; using DoliMiddlewareApi.Exceptions; using DoliMiddlewareApi.Services; using DoliMiddlewareApi.Services.Auth; @@ -7,6 +8,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Diagnostics; using Microsoft.Extensions.Caching.Memory; using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi; // ========================================= // PROGRAM.CS - CONFIGURACIÓN Y DEPENDENCIAS @@ -15,7 +17,8 @@ using Microsoft.IdentityModel.Tokens; var builder = WebApplication.CreateBuilder(args); if (string.IsNullOrEmpty(builder.Configuration["Jwt:Secret"])) - throw new InvalidOperationException("JWT Secret is required in configuration. Set 'Jwt:Secret' in appsettings.json or environment variables."); + throw new InvalidOperationException( + "JWT Secret is required. Set 'Jwt__Secret' environment variable, 'Jwt:Secret' in appsettings, or use 'dotnet user-secrets set Jwt:Secret '."); // ========================================= // 1. CONFIGURACIÓN DE SERVICIOS ASP.NET CORE @@ -47,23 +50,81 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) builder.Services.AddAuthorization(); -// CORS Configuration +// CORS: Allow all origins in dev, configurable in prod +var corsOrigins = builder.Configuration["Cors:AllowedOrigins"]; builder.Services.AddCors(options => { - options.AddPolicy("AllowVueApp", - policy => policy - .WithOrigins( - "http://localhost:3001", - "http://localhost:3000", - "http://localhost:5173") - .AllowAnyMethod() - .AllowAnyHeader() - .AllowCredentials()); + options.AddPolicy("AllowVueApp", policy => + { + if (string.IsNullOrEmpty(corsOrigins)) + { + policy.SetIsOriginAllowed(_ => true) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + } + else + { + var origins = corsOrigins.Split(';', StringSplitOptions.RemoveEmptyEntries); + policy.WithOrigins(origins) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + } + }); }); // Swagger/OpenAPI builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "DoliMiddlewareApi", + Version = "1.0", + Description = "BFF API for Dolibarr ERP - Middleware between frontend and Dolibarr" + }); + + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Scheme = "bearer", + BearerFormat = "JWT", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Description = "Enter your JWT Bearer token (obtained from POST /api/Auth/login)" + }); + + options.AddSecurityRequirement(document => new OpenApiSecurityRequirement + { + [new OpenApiSecuritySchemeReference("Bearer", document)] = [] + }); +}); + +// Health checks (verifica que el BFF responde) +builder.Services.AddHealthChecks(); + +// Rate limiting: proteger login contra brute force +builder.Services.AddRateLimiter(options => +{ + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + options.GlobalLimiter = PartitionedRateLimiter.Create(context => + { + var path = context.Request.Path.Value; + if (path != null && path.StartsWith("/api/Auth/login", StringComparison.OrdinalIgnoreCase)) + { + return RateLimitPartition.GetSlidingWindowLimiter("login", _ => new SlidingWindowRateLimiterOptions + { + PermitLimit = 5, + Window = TimeSpan.FromMinutes(1), + SegmentsPerWindow = 2, + QueueProcessingOrder = QueueProcessingOrder.OldestFirst, + QueueLimit = 0 + }); + } + return RateLimitPartition.GetNoLimiter("default"); + }); +}); // 2. DOLIBARR HTTP CLIENT @@ -82,7 +143,9 @@ builder.Services.AddSwaggerGen(); builder.Services.AddHttpClient("Dolibarr", client => { - client.BaseAddress = new Uri(builder.Configuration["Dolibarr:ApiUrl"]!); + var baseUrl = builder.Configuration["Dolibarr:ApiUrl"]!; + if (!baseUrl.EndsWith('/')) baseUrl += '/'; + client.BaseAddress = new Uri(baseUrl); }); builder.Services.AddScoped(sp => @@ -103,8 +166,15 @@ builder.Services.AddScoped(); // Servicio de negocio (clientes) builder.Services.AddScoped(); +// Servicio de negocio (contactos) +builder.Services.AddScoped(); + +// Servicio de negocio (documentos) builder.Services.AddScoped(); +// Servicio de negocio (setup/diccionarios) +builder.Services.AddScoped(); + // Servicio de aplicación (orquesta login + cache) builder.Services.AddScoped(); @@ -199,17 +269,17 @@ app.UseExceptionHandler(errorApp => // 5. PIPELINE ASP.NET CORE // ========================================= -if (app.Environment.IsDevelopment()) +app.UseSwagger(); +app.UseSwaggerUI(options => { - app.UseSwagger(); - app.UseSwaggerUI(options => - { - options.SwaggerEndpoint("/swagger/v1/swagger.json", "DoliMiddlewareApi v1"); - }); -} + options.SwaggerEndpoint("/swagger/v1/swagger.json", "DoliMiddlewareApi v1"); +}); -app.UseHttpsRedirection(); +// No HTTPS redirect in Docker (behind reverse proxy) +if (!bool.TryParse(builder.Configuration["Dolibarr:ForceHttps"], out var forceHttps) || !forceHttps) + app.UseHttpsRedirection(); +app.UseRateLimiter(); app.UseCors("AllowVueApp"); @@ -218,4 +288,6 @@ app.UseAuthorization(); app.MapControllers(); +app.MapHealthChecks("/health"); + app.Run(); diff --git a/DoliMiddlewareApi/appsettings.Development.json b/DoliMiddlewareApi/appsettings.Development.json index a1234c1..7569655 100644 --- a/DoliMiddlewareApi/appsettings.Development.json +++ b/DoliMiddlewareApi/appsettings.Development.json @@ -5,5 +5,8 @@ "Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None" } + }, + "Jwt": { + "Secret": "dev-secret-key-change-in-production-min-32-chars" } } diff --git a/DoliMiddlewareApi/appsettings.json b/DoliMiddlewareApi/appsettings.json index 693725c..aa06801 100644 --- a/DoliMiddlewareApi/appsettings.json +++ b/DoliMiddlewareApi/appsettings.json @@ -8,11 +8,14 @@ }, "AllowedHosts": "*", "Dolibarr": { - "ApiUrl": "http://localhost/api/index.php/explorer", + "ApiUrl": "http://localhost/api/index.php", "ApiKey": "" }, + "Cors": { + "AllowedOrigins": "" + }, "Jwt": { - "Secret": "mi-super-secreto-jwt-aqui-cambiar-en-produccion", + "Secret": "", "Issuer": "DoliMiddleware", "Audience": "DoliClients" } diff --git a/compose.yaml b/compose.yaml index bb3f951..5f7eed2 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,13 +1,12 @@ - -services: +services: mysql: image: mysql:8.0 container_name: dolibarr-mysql environment: - MYSQL_ROOT_PASSWORD: "rootpassword" + MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:-rootpassword}" MYSQL_DATABASE: "dolibarr" MYSQL_USER: "dolibarr" - MYSQL_PASSWORD: "12345678" + MYSQL_PASSWORD: "${MYSQL_PASSWORD:-12345678}" ports: - "3306:3306" volumes: @@ -20,21 +19,6 @@ services: retries: 5 restart: unless-stopped - # redis: - # image: redis:7-alpine - # container_name: dolibarr-redis - # ports: - # - "6379:6379" - # volumes: - # - redis_data:/data - # command: redis-server --appendonly yes - # healthcheck: - # test: ["CMD", "redis-cli", "ping"] - # interval: 10s - # timeout: 3s - # retries: 5 - # restart: unless-stopped - dolibarr: image: dolibarr/dolibarr:latest container_name: dolibarr-web @@ -46,36 +30,43 @@ services: DOLI_DB_HOST: "mysql" DOLI_DB_HOST_PORT: "3306" DOLI_DB_USER: "dolibarr" - DOLI_DB_PASSWORD: "12345678" + DOLI_DB_PASSWORD: "${MYSQL_PASSWORD:-12345678}" DOLI_DB_NAME: "dolibarr" DOLI_ADMIN_LOGIN: "admin" - DOLI_ADMIN_PASSWORD: "12345678" + DOLI_ADMIN_PASSWORD: "${DOLI_ADMIN_PASSWORD:-12345678}" DOLI_URL_ROOT: "http://localhost" ports: - "80:80" volumes: - dolibarr_documents:/var/www/documents - dolibarr_custom:/var/www/html/custom + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"] + interval: 15s + timeout: 5s + retries: 10 + start_period: 30s restart: unless-stopped - # dolimiddlewareapi: - # image: dolimiddlewareapi - # build: - # context: . - # dockerfile: DoliMiddlewareApi/Dockerfile - # container_name: dolibarr-middleware-api - # depends_on: - # - dolibarr - # - mysql - # ports: - # - "5000:8080" - # environment: - # - ASPNETCORE_ENVIRONMENT=Development - # restart: unless-stopped + dolimiddlewareapi: + image: dolimiddlewareapi + build: + context: . + dockerfile: DoliMiddlewareApi/Dockerfile + container_name: dolibarr-middleware-api + depends_on: + dolibarr: + condition: service_healthy + ports: + - "5000:8080" + environment: + - ASPNETCORE_ENVIRONMENT=Production + - Jwt__Secret=DevDemoSecretKeyForDockerCompose2026Min32Chars!! + - Dolibarr__ApiUrl=http://dolibarr/api/index.php + - Cors__AllowedOrigins=http://localhost:3000;http://localhost:3001;http://localhost:5173;http://localhost:5269 + restart: unless-stopped volumes: mysql_data: - # redis_data: dolibarr_documents: - dolibarr_custom: - + dolibarr_custom: \ No newline at end of file