denuncias

This commit is contained in:
2026-05-21 12:07:51 +02:00
parent 693d950dfa
commit b62cfd46c1
25 changed files with 1805 additions and 418 deletions

View File

@@ -19,7 +19,11 @@ CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo("es-ES");
builder.Services.Configure<ApiDenunciasOptions>(builder.Configuration.GetSection(ApiDenunciasOptions.SectionName));
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
.AddInteractiveServerComponents(options =>
{
options.DetailedErrors = true;
options.JSInteropDefaultCallTimeout = TimeSpan.FromSeconds(180);
});
builder.Services.AddCascadingAuthenticationState();
builder.Services
@@ -37,12 +41,17 @@ builder.Services
builder.Services.AddAuthorization();
builder.Services.AddDataProtection();
builder.Services.AddServerSideBlazor().AddCircuitOptions(option => { option.DetailedErrors = true; });
builder.Services.AddServerSideBlazor().AddCircuitOptions(option =>
{
option.DetailedErrors = true;
option.JSInteropDefaultCallTimeout = TimeSpan.FromSeconds(180);
});
builder.Services.AddHttpContextAccessor();
builder.Services.AddAntiforgery();
builder.Services.AddScoped<UserState>();
builder.Services.AddSingleton<AppSessionLifetime>();
builder.Services.AddSingleton<LoginRateLimiter>();
builder.Services.AddScoped<UiBusyService>();
builder.Services.AddScoped<ApiDenunciasClient>();
builder.Services.AddScoped<IDenunciaStore, ApiDenunciaStore>();
builder.Services.AddScoped<IInboxTrackingService, ApiInboxTrackingService>();
@@ -140,11 +149,60 @@ app.UseAntiforgery();
var api = app.MapGroup("/api");
api.MapPost("/auth/prepare", async (
ApiLoginPrepareRequest request,
ApiDenunciasClient apiClient,
IOptions<ApiDenunciasOptions> apiOptions,
CancellationToken cancellationToken) =>
{
if (string.IsNullOrWhiteSpace(request.Username) ||
string.IsNullOrWhiteSpace(request.Password))
{
return Results.Json(
new ApiError("Debes indicar usuario y contrasena."),
statusCode: StatusCodes.Status400BadRequest);
}
try
{
var loginTimeoutSeconds = Math.Clamp(apiOptions.Value.LoginTimeoutSeconds, 15, 300);
using var loginTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
loginTimeout.CancelAfter(TimeSpan.FromSeconds(loginTimeoutSeconds));
var prepared = await apiClient.PrepareLoginAsync(
request with { Username = request.Username.Trim() },
loginTimeout.Token);
return Results.Ok(prepared);
}
catch (UnauthorizedAccessException ex)
{
return Results.Json(new ApiError(ex.Message), statusCode: StatusCodes.Status401Unauthorized);
}
catch (InvalidOperationException ex)
{
return Results.Json(new ApiError(ex.Message), statusCode: StatusCodes.Status400BadRequest);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
return Results.Json(
new ApiError($"La API de denuncias no ha respondido en {apiOptions.Value.LoginTimeoutSeconds} segundos ({apiOptions.Value.BaseUrl})."),
statusCode: StatusCodes.Status504GatewayTimeout);
}
catch (HttpRequestException ex)
{
return Results.Json(
new ApiError($"No se ha podido conectar con la API de denuncias ({apiOptions.Value.BaseUrl}). Detalle: {ex.Message}"),
statusCode: StatusCodes.Status503ServiceUnavailable);
}
}).DisableAntiforgery();
api.MapPost("/auth/login", async (
LoginRequest request,
HttpContext httpContext,
ApiDenunciasClient apiClient,
LoginRateLimiter rateLimiter,
IOptions<ApiDenunciasOptions> apiOptions,
CancellationToken cancellationToken) =>
{
var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
@@ -174,13 +232,17 @@ api.MapPost("/auth/login", async (
try
{
var loginTimeoutSeconds = Math.Clamp(apiOptions.Value.LoginTimeoutSeconds, 15, 300);
using var loginTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
loginTimeout.CancelAfter(TimeSpan.FromSeconds(loginTimeoutSeconds));
var login = await apiClient.LoginAsync(
request with
{
Username = request.Username.Trim(),
Authcode = request.Authcode.Trim()
},
cancellationToken);
loginTimeout.Token);
var claims = new List<Claim>
{
@@ -220,6 +282,104 @@ api.MapPost("/auth/login", async (
new ApiError(ex.Message),
statusCode: StatusCodes.Status400BadRequest);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
return Results.Json(
new ApiError($"La API de denuncias no ha respondido en {apiOptions.Value.LoginTimeoutSeconds} segundos ({apiOptions.Value.BaseUrl}). Comprueba los logs de ApiDenuncias: probablemente esta esperando a GlobalLeaks o a una dependencia externa."),
statusCode: StatusCodes.Status504GatewayTimeout);
}
catch (HttpRequestException ex)
{
return Results.Json(
new ApiError($"No se ha podido conectar con la API de denuncias ({apiOptions.Value.BaseUrl}). Detalle: {ex.Message}"),
statusCode: StatusCodes.Status503ServiceUnavailable);
}
}).DisableAntiforgery();
api.MapPost("/auth/complete", async (
ApiLoginCompleteRequest request,
HttpContext httpContext,
ApiDenunciasClient apiClient,
IOptions<ApiDenunciasOptions> apiOptions,
CancellationToken cancellationToken) =>
{
var appSessionLifetime = httpContext.RequestServices.GetRequiredService<AppSessionLifetime>();
if (string.IsNullOrWhiteSpace(request.PendingLoginId) ||
string.IsNullOrWhiteSpace(request.Authcode))
{
return Results.Json(
new ApiError("Debes indicar el codigo 2FA."),
statusCode: StatusCodes.Status400BadRequest);
}
if (!Regex.IsMatch(request.Authcode.Trim(), @"^\d{6}$"))
{
return Results.Json(
new ApiError("El codigo 2FA debe tener exactamente 6 digitos."),
statusCode: StatusCodes.Status400BadRequest);
}
try
{
var loginTimeoutSeconds = Math.Clamp(apiOptions.Value.LoginTimeoutSeconds, 15, 300);
using var loginTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
loginTimeout.CancelAfter(TimeSpan.FromSeconds(loginTimeoutSeconds));
var login = await apiClient.CompleteLoginAsync(
request with { Authcode = request.Authcode.Trim() },
loginTimeout.Token);
var claims = new List<Claim>
{
new(ClaimTypes.Name, login.Username),
new("app_startup_stamp", appSessionLifetime.StartupStamp),
new(ApiDenunciasClient.AccessTokenClaim, login.AccessToken),
new(ApiDenunciasClient.TokenExpiresAtClaim, login.ExpiresAtUtc.ToString("O", CultureInfo.InvariantCulture)),
};
if (!string.IsNullOrWhiteSpace(login.Role))
{
claims.Add(new Claim("gl_role", login.Role));
}
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var principal = new ClaimsPrincipal(identity);
var authProperties = new AuthenticationProperties
{
IsPersistent = false,
AllowRefresh = true,
};
await httpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
principal,
authProperties);
return Results.Ok(new LoginResponse(login.Username));
}
catch (UnauthorizedAccessException ex)
{
return Results.Json(new ApiError(ex.Message), statusCode: StatusCodes.Status401Unauthorized);
}
catch (InvalidOperationException ex)
{
return Results.Json(
new ApiError(ex.Message),
statusCode: StatusCodes.Status400BadRequest);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
return Results.Json(
new ApiError($"La API de denuncias no ha respondido en {apiOptions.Value.LoginTimeoutSeconds} segundos ({apiOptions.Value.BaseUrl})."),
statusCode: StatusCodes.Status504GatewayTimeout);
}
catch (HttpRequestException ex)
{
return Results.Json(
new ApiError($"No se ha podido conectar con la API de denuncias ({apiOptions.Value.BaseUrl}). Detalle: {ex.Message}"),
statusCode: StatusCodes.Status503ServiceUnavailable);
}
}).DisableAntiforgery();
api.MapPost("/auth/logout", async (