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:
parent
fbbbce3574
commit
fe6bd57796
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
compose.yaml
65
compose.yaml
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue