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
This commit is contained in:
JavMB 2026-05-15 18:58:43 +02:00
parent fbbbce3574
commit fe6bd57796
7 changed files with 162 additions and 87 deletions

View File

@ -1,25 +1,16 @@
**/.dockerignore bin/
**/.env obj/
**/.git .vs/
**/.gitignore .vscode/
**/.project node_modules/
**/.settings *.user
**/.toolstarget *.suo
**/.vs *.log
**/.vscode .env
**/.idea .env.local
**/*.*proj.user .git/
**/*.dbmdl .gitignore
**/*.jfm docker-compose*.yml
**/azds.yaml *.md
**/bin presentacion/
**/charts DoliMiddlewareApi.Tests/
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

16
.env.example Normal file
View File

@ -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

View File

@ -10,7 +10,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.15.0" /> <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.15.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />

View File

@ -1,4 +1,5 @@
using System.Text; using System.Text;
using System.Threading.RateLimiting;
using DoliMiddlewareApi.Exceptions; using DoliMiddlewareApi.Exceptions;
using DoliMiddlewareApi.Services; using DoliMiddlewareApi.Services;
using DoliMiddlewareApi.Services.Auth; using DoliMiddlewareApi.Services.Auth;
@ -7,6 +8,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Diagnostics;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi;
// ========================================= // =========================================
// PROGRAM.CS - CONFIGURACIÓN Y DEPENDENCIAS // PROGRAM.CS - CONFIGURACIÓN Y DEPENDENCIAS
@ -15,7 +17,8 @@ using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
if (string.IsNullOrEmpty(builder.Configuration["Jwt:Secret"])) 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 <value>'.");
// ========================================= // =========================================
// 1. CONFIGURACIÓN DE SERVICIOS ASP.NET CORE // 1. CONFIGURACIÓN DE SERVICIOS ASP.NET CORE
@ -47,23 +50,81 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
// CORS Configuration // CORS: Allow all origins in dev, configurable in prod
var corsOrigins = builder.Configuration["Cors:AllowedOrigins"];
builder.Services.AddCors(options => builder.Services.AddCors(options =>
{ {
options.AddPolicy("AllowVueApp", options.AddPolicy("AllowVueApp", policy =>
policy => policy {
.WithOrigins( if (string.IsNullOrEmpty(corsOrigins))
"http://localhost:3001", {
"http://localhost:3000", policy.SetIsOriginAllowed(_ => true)
"http://localhost:5173") .AllowAnyMethod()
.AllowAnyMethod() .AllowAnyHeader()
.AllowAnyHeader() .AllowCredentials();
.AllowCredentials()); }
else
{
var origins = corsOrigins.Split(';', StringSplitOptions.RemoveEmptyEntries);
policy.WithOrigins(origins)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
}
});
}); });
// Swagger/OpenAPI // Swagger/OpenAPI
builder.Services.AddEndpointsApiExplorer(); 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<HttpContext, string>(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 // 2. DOLIBARR HTTP CLIENT
@ -82,7 +143,9 @@ builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient("Dolibarr", client => 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<IDolibarrApiClient>(sp => builder.Services.AddScoped<IDolibarrApiClient>(sp =>
@ -103,8 +166,15 @@ builder.Services.AddScoped<InvoiceService>();
// Servicio de negocio (clientes) // Servicio de negocio (clientes)
builder.Services.AddScoped<ClientService>(); builder.Services.AddScoped<ClientService>();
// Servicio de negocio (contactos)
builder.Services.AddScoped<ContactService>();
// Servicio de negocio (documentos)
builder.Services.AddScoped<DocumentService>(); builder.Services.AddScoped<DocumentService>();
// Servicio de negocio (setup/diccionarios)
builder.Services.AddScoped<SetupService>();
// Servicio de aplicación (orquesta login + cache) // Servicio de aplicación (orquesta login + cache)
builder.Services.AddScoped<AuthApplicationService>(); builder.Services.AddScoped<AuthApplicationService>();
@ -199,17 +269,17 @@ app.UseExceptionHandler(errorApp =>
// 5. PIPELINE ASP.NET CORE // 5. PIPELINE ASP.NET CORE
// ========================================= // =========================================
if (app.Environment.IsDevelopment()) app.UseSwagger();
app.UseSwaggerUI(options =>
{ {
app.UseSwagger(); options.SwaggerEndpoint("/swagger/v1/swagger.json", "DoliMiddlewareApi v1");
app.UseSwaggerUI(options => });
{
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"); app.UseCors("AllowVueApp");
@ -218,4 +288,6 @@ app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.MapHealthChecks("/health");
app.Run(); app.Run();

View File

@ -5,5 +5,8 @@
"Microsoft.AspNetCore": "Warning", "Microsoft.AspNetCore": "Warning",
"Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None" "Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware": "None"
} }
},
"Jwt": {
"Secret": "dev-secret-key-change-in-production-min-32-chars"
} }
} }

View File

@ -8,11 +8,14 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"Dolibarr": { "Dolibarr": {
"ApiUrl": "http://localhost/api/index.php/explorer", "ApiUrl": "http://localhost/api/index.php",
"ApiKey": "" "ApiKey": ""
}, },
"Cors": {
"AllowedOrigins": ""
},
"Jwt": { "Jwt": {
"Secret": "mi-super-secreto-jwt-aqui-cambiar-en-produccion", "Secret": "",
"Issuer": "DoliMiddleware", "Issuer": "DoliMiddleware",
"Audience": "DoliClients" "Audience": "DoliClients"
} }

View File

@ -1,13 +1,12 @@
 services:
services:
mysql: mysql:
image: mysql:8.0 image: mysql:8.0
container_name: dolibarr-mysql container_name: dolibarr-mysql
environment: environment:
MYSQL_ROOT_PASSWORD: "rootpassword" MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD:-rootpassword}"
MYSQL_DATABASE: "dolibarr" MYSQL_DATABASE: "dolibarr"
MYSQL_USER: "dolibarr" MYSQL_USER: "dolibarr"
MYSQL_PASSWORD: "12345678" MYSQL_PASSWORD: "${MYSQL_PASSWORD:-12345678}"
ports: ports:
- "3306:3306" - "3306:3306"
volumes: volumes:
@ -20,21 +19,6 @@ services:
retries: 5 retries: 5
restart: unless-stopped 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: dolibarr:
image: dolibarr/dolibarr:latest image: dolibarr/dolibarr:latest
container_name: dolibarr-web container_name: dolibarr-web
@ -46,36 +30,43 @@ services:
DOLI_DB_HOST: "mysql" DOLI_DB_HOST: "mysql"
DOLI_DB_HOST_PORT: "3306" DOLI_DB_HOST_PORT: "3306"
DOLI_DB_USER: "dolibarr" DOLI_DB_USER: "dolibarr"
DOLI_DB_PASSWORD: "12345678" DOLI_DB_PASSWORD: "${MYSQL_PASSWORD:-12345678}"
DOLI_DB_NAME: "dolibarr" DOLI_DB_NAME: "dolibarr"
DOLI_ADMIN_LOGIN: "admin" DOLI_ADMIN_LOGIN: "admin"
DOLI_ADMIN_PASSWORD: "12345678" DOLI_ADMIN_PASSWORD: "${DOLI_ADMIN_PASSWORD:-12345678}"
DOLI_URL_ROOT: "http://localhost" DOLI_URL_ROOT: "http://localhost"
ports: ports:
- "80:80" - "80:80"
volumes: volumes:
- dolibarr_documents:/var/www/documents - dolibarr_documents:/var/www/documents
- dolibarr_custom:/var/www/html/custom - 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 restart: unless-stopped
# dolimiddlewareapi: dolimiddlewareapi:
# image: dolimiddlewareapi image: dolimiddlewareapi
# build: build:
# context: . context: .
# dockerfile: DoliMiddlewareApi/Dockerfile dockerfile: DoliMiddlewareApi/Dockerfile
# container_name: dolibarr-middleware-api container_name: dolibarr-middleware-api
# depends_on: depends_on:
# - dolibarr dolibarr:
# - mysql condition: service_healthy
# ports: ports:
# - "5000:8080" - "5000:8080"
# environment: environment:
# - ASPNETCORE_ENVIRONMENT=Development - ASPNETCORE_ENVIRONMENT=Production
# restart: unless-stopped - 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: volumes:
mysql_data: mysql_data:
# redis_data:
dolibarr_documents: dolibarr_documents:
dolibarr_custom: dolibarr_custom: