Files
Antifraude.Net/Antifraude.Net/ApiDenuncias/Controllers/InboxController.cs
Pedro b6c61238a5 Añadir endpoint de campos para Gestiona y adaptar login GlobalLeaks a DPoP
- Añade GET /api/denuncias/{id}/gestiona-fields para exponer los campos diarios requeridos por la integración con Gestiona.
- Devuelve un DTO cerrado con fecha, canal, resumen, datos de hechos, protección, sexo y preferencias de notificación.
- Adapta el login contra GlobalLeaks al nuevo flujo DPoP exigido desde la versión 5.0.94.
- Genera proof DPoP con clave EC P-256 efímera y lo envía en la cabecera DPoP junto a X-Token.
- Mejora el mensaje de error cuando GlobalLeaks rechaza el proof DPoP.
2026-07-09 14:55:29 +02:00

400 lines
16 KiB
C#

using GestionaDenuncias.Shared.Models;
using ApiDenuncias.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ApiDenuncias.Controllers;
[ApiController]
[Authorize]
[Route("api/inbox")]
public sealed class InboxController : ControllerBase
{
private static readonly TimeSpan[] ExportRetryDelays =
[
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
];
private readonly GlobalLeaksSessionStore _sessionStore;
private readonly PendingGlobalLeaksLoginStore _pendingLoginStore;
private readonly GlobalLeaksClient _globalLeaksClient;
private readonly DenunciaInboxService _inboxService;
private readonly IInboxTrackingService _trackingService;
private readonly ILogger<InboxController> _logger;
public InboxController(
GlobalLeaksSessionStore sessionStore,
PendingGlobalLeaksLoginStore pendingLoginStore,
GlobalLeaksClient globalLeaksClient,
DenunciaInboxService inboxService,
IInboxTrackingService trackingService,
ILogger<InboxController> logger)
{
_sessionStore = sessionStore;
_pendingLoginStore = pendingLoginStore;
_globalLeaksClient = globalLeaksClient;
_inboxService = inboxService;
_trackingService = trackingService;
_logger = logger;
}
[HttpGet("session")]
public async Task<ActionResult<ApiGlobalLeaksSessionDto?>> GetSession(CancellationToken cancellationToken)
{
var session = await _sessionStore.GetAsync(GetUsername(), cancellationToken);
return Ok(ToDto(session));
}
[HttpPost("session/renew/prepare")]
public async Task<ActionResult<ApiLoginPrepareResponse>> PrepareRenewSession(CancellationToken cancellationToken)
{
var username = GetUsername();
var current = await _sessionStore.GetAsync(username, cancellationToken);
if (current is null || string.IsNullOrWhiteSpace(current.Password))
{
return BadRequest(new ApiError("No hay credenciales guardadas para este usuario. Cierra sesion y vuelve a entrar."));
}
try
{
var prepared = await _globalLeaksClient.PrepareLoginAsync(
current.Username,
current.Password,
cancellationToken);
var pending = _pendingLoginStore.Create(
prepared.Username,
current.Password,
prepared.FinalPassword,
prepared.TokenAnswer);
return Ok(new ApiLoginPrepareResponse(pending.Id, pending.Username, pending.ExpiresAtUtc));
}
catch (GlobalLeaksValidationException ex)
{
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
}
catch (Exception ex)
{
_logger.LogError(ex, "No se ha podido preparar la renovacion GlobalLeaks para {Username}.", username);
return StatusCode(
StatusCodes.Status500InternalServerError,
new ApiError("No se ha podido preparar la renovacion de GlobalLeaks. Intentalo de nuevo en unos segundos."));
}
}
[HttpPost("session/renew")]
public async Task<ActionResult<ApiGlobalLeaksSessionDto>> RenewSession(
RenewGlobalLeaksSessionRequest request,
CancellationToken cancellationToken)
{
var username = GetUsername();
var current = await _sessionStore.GetAsync(username, cancellationToken);
if (current is null || string.IsNullOrWhiteSpace(current.Password))
{
return BadRequest(new ApiError("No hay credenciales guardadas para este usuario. Cierra sesion y vuelve a entrar."));
}
if (string.IsNullOrWhiteSpace(request.Authcode) || request.Authcode.Trim().Length != 6)
{
return BadRequest(new ApiError("Introduce un codigo 2FA valido de 6 digitos."));
}
try
{
GlSession session;
if (!string.IsNullOrWhiteSpace(request.PendingLoginId))
{
var pending = _pendingLoginStore.Get(request.PendingLoginId);
if (!string.Equals(pending.Username, current.Username, StringComparison.OrdinalIgnoreCase))
{
return BadRequest(new ApiError("La preparacion del login no corresponde al usuario actual."));
}
session = await _globalLeaksClient.CompleteLoginAsync(
pending.Username,
pending.FinalPassword,
pending.TokenAnswer,
request.Authcode.Trim(),
cancellationToken);
_pendingLoginStore.Remove(pending.Id);
}
else
{
session = await _globalLeaksClient.LoginAsync(
current.Username,
current.Password,
request.Authcode.Trim(),
cancellationToken);
}
await _sessionStore.UpdateSessionAsync(username, session.Id, session.Role, cancellationToken);
var stored = await _sessionStore.GetAsync(username, cancellationToken);
return Ok(ToDto(stored));
}
catch (GlobalLeaksValidationException ex)
{
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
}
catch (InvalidOperationException ex)
{
return BadRequest(new ApiError(ex.Message));
}
catch (Exception ex)
{
_logger.LogError(ex, "No se ha podido cargar la bandeja GlobalLeaks para {Username}.", username);
return StatusCode(
StatusCodes.Status500InternalServerError,
new ApiError("No se ha podido renovar la sesion de GlobalLeaks. Intentalo de nuevo en unos segundos."));
}
}
[HttpPost("session/clear")]
public async Task<IActionResult> ClearSession(CancellationToken cancellationToken)
{
await _sessionStore.ClearSessionAsync(GetUsername(), cancellationToken);
return Ok(new { ok = true });
}
[HttpGet("reports")]
public async Task<ActionResult<InboxSnapshotResponse>> GetReports(CancellationToken cancellationToken)
{
var username = GetUsername();
var session = await RequireActiveSessionAsync(username, cancellationToken);
if (session is null)
{
return Unauthorized(new ApiError("La sesion de GlobalLeaks ha caducado. Renueva el 2FA.", SessionExpired: true));
}
try
{
var state = await _trackingService.GetUserStateAsync(username, cancellationToken);
var contexts = await _globalLeaksClient.GetContextsAsync(session.SessionId!, cancellationToken);
var reports = await _globalLeaksClient.GetReportsAsync(session.SessionId!, "all", null, null, cancellationToken, contexts);
var enrichedReports = await _trackingService.RegisterSnapshotAsync(username, reports, cancellationToken);
var activityReports = await _globalLeaksClient.EnrichReportsWithActivityAsync(
session.SessionId!,
enrichedReports,
state.LastDownloadedReportMomentUtc,
cancellationToken);
return Ok(new InboxSnapshotResponse(contexts, activityReports, state));
}
catch (GlobalLeaksSessionExpiredException)
{
await _sessionStore.ClearSessionAsync(username, cancellationToken);
return Unauthorized(new ApiError("La sesion de GlobalLeaks ha caducado. Renueva el 2FA.", SessionExpired: true));
}
catch (GlobalLeaksValidationException ex)
{
return ToGlobalLeaksApiError(ex, "cargar la bandeja de GlobalLeaks");
}
catch (Exception ex)
{
_logger.LogError(ex, "No se ha podido cargar la bandeja GlobalLeaks para {Username}.", username);
return StatusCode(
StatusCodes.Status500InternalServerError,
new ApiError("No se ha podido cargar la bandeja de GlobalLeaks. Intentalo de nuevo en unos segundos."));
}
}
[HttpPost("reports/{reportId}/import")]
public async Task<ActionResult<ImportSummary>> ImportReport(
string reportId,
ImportReportRequest request,
CancellationToken cancellationToken)
{
var username = GetUsername();
var session = await RequireActiveSessionAsync(username, cancellationToken);
if (session is null)
{
return Unauthorized(new ApiError("La sesion de GlobalLeaks ha caducado. Renueva el 2FA.", SessionExpired: true));
}
var report = string.IsNullOrWhiteSpace(request.Report.Id)
? request.Report with { Id = reportId }
: request.Report;
try
{
await _trackingService.EnsureReportCanBeImportedByUserAsync(username, report, cancellationToken);
ReportDetailDto? reportDetail = null;
try
{
reportDetail = await _globalLeaksClient.GetReportDetailAsync(
session.SessionId!,
report.Id,
report.LastAccess,
cancellationToken);
}
catch (GlobalLeaksValidationException ex) when (ex.StatusCode == StatusCodes.Status422UnprocessableEntity ||
ex.StatusCode is >= 500 and <= 504)
{
_logger.LogWarning(
ex,
"No se ha podido obtener el detalle de fechas de adjuntos para la denuncia {ReportId}. Se usara la fecha del paquete exportado.",
report.Id);
}
var reportPackage = await DownloadReportPackageWithRetryAsync(session.SessionId!, report, cancellationToken);
FileDownloadResult? json = null;
try
{
json = await _globalLeaksClient.ExportReportJsonAsync(session.SessionId!, report.Id, cancellationToken);
}
catch (GlobalLeaksValidationException ex) when (ex.StatusCode == 422)
{
json = null;
}
var result = await _inboxService.ImportFromGlobalLeaksAsync(reportPackage, json, reportDetail, cancellationToken);
if (result.ImportedCount > 0)
{
await _trackingService.MarkReportImportedAsync(
username,
report,
result.ImportedComplaintIds?.FirstOrDefault(),
cancellationToken);
}
return Ok(result);
}
catch (GlobalLeaksSessionExpiredException)
{
await _sessionStore.ClearSessionAsync(username, cancellationToken);
return Unauthorized(new ApiError("La sesion de GlobalLeaks ha caducado durante la importacion. Renueva el 2FA.", SessionExpired: true));
}
catch (GlobalLeaksValidationException ex)
{
return ToGlobalLeaksApiError(ex, "importar la denuncia");
}
catch (Exception ex)
{
_logger.LogError(ex, "No se ha podido importar la denuncia {ReportId} para {Username}.", reportId, username);
return StatusCode(
StatusCodes.Status500InternalServerError,
new ApiError("No se ha podido importar la denuncia. Intentalo de nuevo en unos segundos."));
}
}
[HttpGet("reports/{reportId}/detail")]
public async Task<ActionResult<ReportDetailDto>> GetReportDetail(
string reportId,
[FromQuery] string? lastAccess,
CancellationToken cancellationToken)
{
var username = GetUsername();
var session = await RequireActiveSessionAsync(username, cancellationToken);
if (session is null)
{
return Unauthorized(new ApiError("La sesion de GlobalLeaks ha caducado. Renueva el 2FA.", SessionExpired: true));
}
try
{
return Ok(await _globalLeaksClient.GetReportDetailAsync(session.SessionId!, reportId, lastAccess, cancellationToken));
}
catch (GlobalLeaksSessionExpiredException)
{
await _sessionStore.ClearSessionAsync(username, cancellationToken);
return Unauthorized(new ApiError("La sesion de GlobalLeaks ha caducado. Renueva el 2FA.", SessionExpired: true));
}
catch (GlobalLeaksValidationException ex)
{
return ToGlobalLeaksApiError(ex, "leer el detalle de la denuncia");
}
catch (Exception ex)
{
_logger.LogError(ex, "No se ha podido leer el detalle de la denuncia {ReportId} para {Username}.", reportId, username);
return StatusCode(
StatusCodes.Status500InternalServerError,
new ApiError("No se ha podido abrir el detalle de la denuncia. Intentalo de nuevo en unos segundos."));
}
}
private async Task<FileDownloadResult> DownloadReportPackageWithRetryAsync(
string sessionId,
ReportDto report,
CancellationToken cancellationToken)
{
for (var attempt = 0; ; attempt++)
{
try
{
return await _globalLeaksClient.DownloadReportPackageAsync(sessionId, report.Id, cancellationToken);
}
catch (GlobalLeaksValidationException ex) when (IsExportNotReady(ex) && attempt < ExportRetryDelays.Length)
{
var delay = ExportRetryDelays[attempt];
_logger.LogWarning(
ex,
"GlobalLeaks aun no ha preparado la exportacion de la denuncia {ReportId}. Reintento {Attempt}/{Total} en {DelaySeconds}s.",
report.Id,
attempt + 1,
ExportRetryDelays.Length,
delay.TotalSeconds);
await Task.Delay(delay, cancellationToken);
}
catch (GlobalLeaksValidationException ex) when (IsExportNotReady(ex))
{
throw new GlobalLeaksValidationException(
"GlobalLeaks todavia esta preparando la exportacion de esta denuncia. Espera unos segundos y vuelve a importarla.",
StatusCodes.Status503ServiceUnavailable);
}
}
}
private static bool IsExportNotReady(GlobalLeaksValidationException ex)
=> ex.StatusCode is >= 500 and <= 504;
private static ActionResult ToGlobalLeaksApiError(GlobalLeaksValidationException ex, string operation)
{
if (ex.StatusCode == StatusCodes.Status401Unauthorized ||
ex.StatusCode == StatusCodes.Status403Forbidden)
{
return new ObjectResult(new ApiError(
"No tienes permiso en GlobalLeaks para acceder a esa denuncia. Puede ser una denuncia anterior a la creacion de tu usuario; consulta con el administrador del buzon."))
{
StatusCode = StatusCodes.Status403Forbidden
};
}
if (ex.StatusCode is >= 500 and <= 504)
{
return new ObjectResult(new ApiError(
$"GlobalLeaks no ha podido {operation} en este momento. Intentalo de nuevo en unos segundos."))
{
StatusCode = StatusCodes.Status503ServiceUnavailable
};
}
return new ObjectResult(new ApiError(ex.Message))
{
StatusCode = ex.StatusCode
};
}
private async Task<GlobalLeaksStoredSession?> RequireActiveSessionAsync(string username, CancellationToken cancellationToken)
{
var session = await _sessionStore.GetAsync(username, cancellationToken);
return session?.HasActiveSession == true ? session : null;
}
private string GetUsername()
=> User.Identity?.Name ?? throw new InvalidOperationException("No hay usuario autenticado.");
private static ApiGlobalLeaksSessionDto? ToDto(GlobalLeaksStoredSession? session)
=> session is null
? null
: new ApiGlobalLeaksSessionDto(session.Username, session.Role, session.HasActiveSession, session.UpdatedAt);
}