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
**/.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
bin/
obj/
.vs/
.vscode/
node_modules/
*.user
*.suo
*.log
.env
.env.local
.git/
.gitignore
docker-compose*.yml
*.md
presentacion/
DoliMiddlewareApi.Tests/

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>
<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="Swashbuckle.AspNetCore" Version="10.1.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.15.0" />

View File

@ -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 <value>'.");
// =========================================
// 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<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
@ -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<IDolibarrApiClient>(sp =>
@ -103,8 +166,15 @@ builder.Services.AddScoped<InvoiceService>();
// Servicio de negocio (clientes)
builder.Services.AddScoped<ClientService>();
// Servicio de negocio (contactos)
builder.Services.AddScoped<ContactService>();
// Servicio de negocio (documentos)
builder.Services.AddScoped<DocumentService>();
// Servicio de negocio (setup/diccionarios)
builder.Services.AddScoped<SetupService>();
// Servicio de aplicación (orquesta login + cache)
builder.Services.AddScoped<AuthApplicationService>();
@ -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();

View File

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

View File

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

View File

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