diff --git a/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs b/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs index 5d21881..f9b266c 100644 --- a/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs +++ b/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs @@ -82,13 +82,42 @@ public sealed class DenunciasController : ControllerBase return NotFound(new ApiError("No se ha encontrado la denuncia solicitada.")); } + return BuildGestionaFieldsResponse(denuncia); + } + + [AllowAnonymous] + [HttpGet("{numeroExpediente:int}/{anioExpediente:int}/gestiona-fields")] + public async Task> GetGestionaFieldsByExpediente( + int numeroExpediente, + int anioExpediente, + CancellationToken cancellationToken) + { + if (numeroExpediente <= 0 || anioExpediente <= 0) + { + return BadRequest(new ApiError("Debes indicar un numero de expediente de Gestiona valido.")); + } + + var expedienteGestiona = $"{numeroExpediente.ToString(CultureInfo.InvariantCulture)}/{anioExpediente.ToString(CultureInfo.InvariantCulture)}"; + var denuncia = await _filteredDenunciaStore.GetDenunciaByGestionaFileCodeAsync( + expedienteGestiona, + cancellationToken); + if (denuncia is null) + { + return NotFound(new ApiError("No se ha encontrado una denuncia asociada al expediente de Gestiona solicitado.")); + } + + return BuildGestionaFieldsResponse(denuncia); + } + + private ActionResult BuildGestionaFieldsResponse(DenunciasGestiona denuncia) + { var response = ToGestionaComplaintFields(denuncia); var missingFields = GetMissingGestionaFields(response); if (missingFields.Count > 0) { _logger.LogInformation( "Campos Gestiona denuncia {DenunciaId}: campos vacios={MissingFields}; rawReportLength={RawReportLength}; formFieldsCount={FormFieldsCount}.", - denunciaId, + denuncia.Id_Denuncia, string.Join(", ", missingFields), denuncia.TextoOriginalReport?.Length ?? 0, denuncia.GetCamposFormulario().Count); diff --git a/Antifraude.Net/ApiDenuncias/Controllers/InboxController.cs b/Antifraude.Net/ApiDenuncias/Controllers/InboxController.cs index fc21766..7dc6461 100644 --- a/Antifraude.Net/ApiDenuncias/Controllers/InboxController.cs +++ b/Antifraude.Net/ApiDenuncias/Controllers/InboxController.cs @@ -386,16 +386,6 @@ public sealed class InboxController : ControllerBase ex.StatusCode, ex.Message); - if (operation.Contains("bandeja", StringComparison.OrdinalIgnoreCase)) - { - return new ObjectResult(new ApiError( - "GlobalLeaks ha rechazado la sesion al cargar la bandeja. Renueva el 2FA y vuelve a intentarlo.", - SessionExpired: true)) - { - StatusCode = StatusCodes.Status401Unauthorized - }; - } - if (IsSessionAuthorizationProblem(ex.Message)) { return new ObjectResult(new ApiError( @@ -406,8 +396,30 @@ public sealed class InboxController : ControllerBase }; } + if (IsDpopValidationProblem(ex.Message)) + { + return new ObjectResult(new ApiError( + "GlobalLeaks no ha podido validar la proteccion de la sesion (DPoP). La sesion no se ha marcado como caducada. Vuelve a intentarlo y, si se repite, avisa al equipo tecnico.")) + { + StatusCode = StatusCodes.Status502BadGateway + }; + } + + if (IsMissingAuthenticationHeadersProblem(ex.Message)) + { + return new ObjectResult(new ApiError( + "No se ha podido completar la autenticacion de la llamada a GlobalLeaks. La sesion no se ha marcado como caducada. Vuelve a intentarlo y, si se repite, avisa al equipo tecnico.")) + { + StatusCode = StatusCodes.Status502BadGateway + }; + } + + var permissionMessage = operation.Contains("bandeja", StringComparison.OrdinalIgnoreCase) + ? "GlobalLeaks no ha autorizado la carga de la bandeja para este usuario. La sesion sigue activa; comprueba los permisos del usuario en el buzon." + : "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."; + 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.")) + permissionMessage)) { StatusCode = StatusCodes.Status403Forbidden }; @@ -430,10 +442,19 @@ public sealed class InboxController : ControllerBase private static bool IsSessionAuthorizationProblem(string? message) => !string.IsNullOrWhiteSpace(message) && - (message.Contains("Invalid DPoP proof", StringComparison.OrdinalIgnoreCase) || - message.Contains("No token and no session", StringComparison.OrdinalIgnoreCase) || + (message.Contains("NotAuthenticated", StringComparison.OrdinalIgnoreCase) || + message.Contains("Invalid token", StringComparison.OrdinalIgnoreCase) || message.Contains("Invalid session", StringComparison.OrdinalIgnoreCase) || - message.Contains("session", StringComparison.OrdinalIgnoreCase) && message.Contains("expired", StringComparison.OrdinalIgnoreCase)); + message.Contains("session", StringComparison.OrdinalIgnoreCase) && message.Contains("expired", StringComparison.OrdinalIgnoreCase) || + message.Contains("token", StringComparison.OrdinalIgnoreCase) && message.Contains("expired", StringComparison.OrdinalIgnoreCase)); + + private static bool IsDpopValidationProblem(string? message) + => !string.IsNullOrWhiteSpace(message) && + message.Contains("Invalid DPoP proof", StringComparison.OrdinalIgnoreCase); + + private static bool IsMissingAuthenticationHeadersProblem(string? message) + => !string.IsNullOrWhiteSpace(message) && + message.Contains("No token and no session", StringComparison.OrdinalIgnoreCase); private async Task RequireActiveSessionAsync(string username, CancellationToken cancellationToken) { diff --git a/Antifraude.Net/ApiDenuncias/Services/EncryptedDenunciaStore.cs b/Antifraude.Net/ApiDenuncias/Services/EncryptedDenunciaStore.cs index 847769e..f8d8e6e 100644 --- a/Antifraude.Net/ApiDenuncias/Services/EncryptedDenunciaStore.cs +++ b/Antifraude.Net/ApiDenuncias/Services/EncryptedDenunciaStore.cs @@ -198,6 +198,14 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore, IFilteredDenunciaSt return denuncia is null ? null : await UnprotectComplaintAsync(denuncia, [], cancellationToken); } + public async Task GetDenunciaByGestionaFileCodeAsync( + string gestionaFileCode, + CancellationToken cancellationToken = default) + { + var denuncia = await _inner.GetDenunciaByGestionaFileCodeAsync(gestionaFileCode, cancellationToken); + return denuncia is null ? null : await UnprotectComplaintAsync(denuncia, [], cancellationToken); + } + public async Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default) { var key = await _envelopeKeyProvider.GetCurrentKeyAsync(cancellationToken); diff --git a/Antifraude.Net/ApiDenuncias/Services/GlobalLeaksClient.cs b/Antifraude.Net/ApiDenuncias/Services/GlobalLeaksClient.cs index 089847b..7e7d588 100644 --- a/Antifraude.Net/ApiDenuncias/Services/GlobalLeaksClient.cs +++ b/Antifraude.Net/ApiDenuncias/Services/GlobalLeaksClient.cs @@ -667,6 +667,10 @@ public sealed class GlobalLeaksClient var response = await _httpClient.SendAsync(request, completionOption, cancellationToken); if ((int)response.StatusCode == 412) { + _logger.LogWarning( + "GlobalLeaks ha indicado sesion expirada (412) en {Method} {Path}.", + request.Method.Method, + request.RequestUri?.OriginalString); response.Dispose(); throw new GlobalLeaksSessionExpiredException(); } @@ -704,22 +708,8 @@ public sealed class GlobalLeaksClient return await SendGlRequestAsync(requestWithoutDpop, cancellationToken, completionOption); } - try - { - using var requestWithDpop = CreateAuthenticatedRequest(method, path, sessionId, dpopPrivateKey); - return await SendGlRequestAsync(requestWithDpop, cancellationToken, completionOption); - } - catch (GlobalLeaksValidationException ex) when (ex.StatusCode is StatusCodes.Status401Unauthorized or StatusCodes.Status403Forbidden) - { - _logger.LogWarning( - ex, - "GlobalLeaks ha rechazado {Method} {Path} con DPoP. Se reintentara una vez solo con X-Session para compatibilidad.", - method.Method, - path); - - using var fallbackRequest = CreateAuthenticatedRequest(method, path, sessionId, dpopPrivateKey: null); - return await SendGlRequestAsync(fallbackRequest, cancellationToken, completionOption); - } + using var requestWithDpop = CreateAuthenticatedRequest(method, path, sessionId, dpopPrivateKey); + return await SendGlRequestAsync(requestWithDpop, cancellationToken, completionOption); } private static HttpRequestMessage CreateRequest(HttpMethod method, string path) diff --git a/Antifraude.Net/ApiDenuncias/Services/IFilteredDenunciaStore.cs b/Antifraude.Net/ApiDenuncias/Services/IFilteredDenunciaStore.cs index 4168822..6ef68d7 100644 --- a/Antifraude.Net/ApiDenuncias/Services/IFilteredDenunciaStore.cs +++ b/Antifraude.Net/ApiDenuncias/Services/IFilteredDenunciaStore.cs @@ -4,6 +4,10 @@ namespace ApiDenuncias.Services; public interface IFilteredDenunciaStore { + Task GetDenunciaByGestionaFileCodeAsync( + string gestionaFileCode, + CancellationToken cancellationToken = default); + Task> GetDenunciasByIdsAsync( IReadOnlyCollection denunciaIds, CancellationToken cancellationToken = default); diff --git a/Antifraude.Net/ApiDenuncias/Services/MySqlDenunciaStore.cs b/Antifraude.Net/ApiDenuncias/Services/MySqlDenunciaStore.cs index 0e956cf..d897429 100644 --- a/Antifraude.Net/ApiDenuncias/Services/MySqlDenunciaStore.cs +++ b/Antifraude.Net/ApiDenuncias/Services/MySqlDenunciaStore.cs @@ -551,6 +551,38 @@ public sealed class MySqlDenunciaStore : IDenunciaStore : null; } + public async Task GetDenunciaByGestionaFileCodeAsync( + string gestionaFileCode, + CancellationToken cancellationToken = default) + { + await EnsureSchemaReadyAsync(cancellationToken); + + var sql = $""" + SELECT + {ComplaintSelectColumns} + FROM complaints + WHERE external_report_id = ( + SELECT history.external_report_id + FROM gestiona_upload_history history + WHERE history.gestiona_file_code = @gestionaFileCode + ORDER BY history.uploaded_at_utc DESC, history.id DESC + LIMIT 1 + ) + OR gestiona_file_code = @gestionaFileCode + ORDER BY COALESCE(gestiona_uploaded_at_utc, report_date_utc) DESC, external_report_id DESC + LIMIT 1; + """; + + await using var connection = await OpenConnectionAsync(cancellationToken); + await using var command = new MySqlCommand(sql, connection); + command.Parameters.AddWithValue("@gestionaFileCode", gestionaFileCode.Trim()); + + await using var reader = await command.ExecuteReaderAsync(cancellationToken); + return await reader.ReadAsync(cancellationToken) + ? MapComplaint(reader) + : null; + } + public async Task AddGestionaUploadHistoryAsync( GestionaUploadHistoryCreateRequest request, string username,