Mejoras en elevaciones, equilibrado y callejero inverso
Añadida visualización de pendientes en los tramos a pie con puntos coloreados: llano, subida y bajada. Añadidos umbrales configurables para subida y bajada junto al interruptor de elevaciones. Añadido callejero inverso en el detalle de la solución, mostrando el lugar del callejero más cercano al origen y al destino. Ajustado el modo Equilib. A para limpiar la tarjeta visualmente y guardar/restaurar el factor usado desde el historial. Mejoras menores en el refresco del resumen técnico de rutas y validación de compilación.
This commit is contained in:
@@ -304,7 +304,7 @@ public sealed class AuthController : ControllerBase
|
|||||||
: session.Username.Trim();
|
: session.Username.Trim();
|
||||||
|
|
||||||
_logger.LogInformation("Login GlobalLeaks validado para {Username}. Guardando sesion cifrada.", username);
|
_logger.LogInformation("Login GlobalLeaks validado para {Username}. Guardando sesion cifrada.", username);
|
||||||
await _sessionStore.SaveAsync(username, password, session.Id, session.Role, cancellationToken);
|
await _sessionStore.SaveAsync(username, password, session.Id, session.Role, session.DpopPrivateKey, cancellationToken);
|
||||||
_logger.LogInformation("Sesion GlobalLeaks guardada para {Username}. Generando JWT.", username);
|
_logger.LogInformation("Sesion GlobalLeaks guardada para {Username}. Generando JWT.", username);
|
||||||
|
|
||||||
var expiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(Math.Max(5, _jwtOptions.ExpirationMinutes));
|
var expiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(Math.Max(5, _jwtOptions.ExpirationMinutes));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using ApiDenuncias.Services;
|
using ApiDenuncias.Services;
|
||||||
using GestionaDenuncias.Shared.Models;
|
using GestionaDenuncias.Shared.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -64,8 +65,9 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
return Ok(await _denunciaStore.GetDenunciaByIdAsync(denunciaId, cancellationToken));
|
return Ok(await _denunciaStore.GetDenunciaByIdAsync(denunciaId, cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AllowAnonymous]
|
||||||
[HttpGet("{denunciaId:int}/gestiona-fields")]
|
[HttpGet("{denunciaId:int}/gestiona-fields")]
|
||||||
public async Task<ActionResult<GestionaComplaintFieldsResponse>> GetGestionaFields(
|
public async Task<ActionResult<GestionaExternalFieldsResponse>> GetGestionaFields(
|
||||||
int denunciaId,
|
int denunciaId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -80,7 +82,19 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
return NotFound(new ApiError("No se ha encontrado la denuncia solicitada."));
|
return NotFound(new ApiError("No se ha encontrado la denuncia solicitada."));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(ToGestionaComplaintFields(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,
|
||||||
|
string.Join(", ", missingFields),
|
||||||
|
denuncia.TextoOriginalReport?.Length ?? 0,
|
||||||
|
denuncia.GetCamposFormulario().Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(ToExternalGestionaFields(response));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -234,18 +248,9 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
|
|
||||||
private static GestionaComplaintFieldsResponse ToGestionaComplaintFields(DenunciasGestiona denuncia)
|
private static GestionaComplaintFieldsResponse ToGestionaComplaintFields(DenunciasGestiona denuncia)
|
||||||
{
|
{
|
||||||
var preferenciaNotificacion = ResolveReportField(
|
var preferenciaNotificacion = ResolveNotificationPreference(denuncia);
|
||||||
denuncia,
|
var seguimiento = ResolveSeguimientoDenuncia(denuncia);
|
||||||
denuncia.Notificacion_Preferencia,
|
var sms = ResolveSmsNotification(denuncia);
|
||||||
"preferencia de notificacion",
|
|
||||||
"seleccione su preferencia de notificacion y seguimiento de su denuncia",
|
|
||||||
"notificaciones");
|
|
||||||
|
|
||||||
var seguimiento = ResolveReportField(
|
|
||||||
denuncia,
|
|
||||||
denuncia.SeguimientoOnline,
|
|
||||||
"seguimiento online",
|
|
||||||
"seguimiento de su denuncia");
|
|
||||||
|
|
||||||
return new GestionaComplaintFieldsResponse(
|
return new GestionaComplaintFieldsResponse(
|
||||||
FechaDenuncia: ToNullableDate(denuncia.Fecha),
|
FechaDenuncia: ToNullableDate(denuncia.Fecha),
|
||||||
@@ -255,29 +260,38 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
ResolveReportField(denuncia, string.Empty, "resumen de la denuncia", "resumen denuncia"),
|
ResolveReportField(denuncia, string.Empty, "resumen de la denuncia", "resumen denuncia"),
|
||||||
ResolveReportField(denuncia, denuncia.Descripcion_Denuncia, "describa su denuncia", "descripcion de la denuncia")),
|
ResolveReportField(denuncia, denuncia.Descripcion_Denuncia, "describa su denuncia", "descripcion de la denuncia")),
|
||||||
FechaHechos: ResolveFechaHechos(denuncia),
|
FechaHechos: ResolveFechaHechos(denuncia),
|
||||||
LugarHechos: ResolveReportField(
|
LugarHechos: ResolveLugarHechos(denuncia),
|
||||||
denuncia,
|
|
||||||
denuncia.Lugar_Hechos,
|
|
||||||
"lugar en el que ocurrieron los hechos que denuncia",
|
|
||||||
"lugar en la que ocurrieron los hechos que denuncia",
|
|
||||||
"lugar de los hechos"),
|
|
||||||
AmbitoCompetencias: ResolveAmbitoCompetencias(denuncia),
|
AmbitoCompetencias: ResolveAmbitoCompetencias(denuncia),
|
||||||
SolicitaProteccion: ResolveReportField(denuncia, denuncia.SolicitaProteccion, "solicita medidas concretas de proteccion", "solicita proteccion"),
|
SolicitaProteccion: ResolveReportField(denuncia, denuncia.SolicitaProteccion, "solicita medidas concretas de proteccion", "solicita proteccion"),
|
||||||
SexoDenunciante: ResolveReportField(denuncia, denuncia.Sexo, "sexo"),
|
SexoDenunciante: ResolveReportField(denuncia, denuncia.Sexo, "sexo"),
|
||||||
AutorizaRemisionDenuncia: ResolveReportField(denuncia, denuncia.AutorizaRemision, "autorizacion para remitir su denuncia", "autoriza remision de la denuncia"),
|
AutorizaRemisionDenuncia: ResolveReportField(denuncia, denuncia.AutorizaRemision, "autorizacion para remitir su denuncia", "autoriza remision de la denuncia"),
|
||||||
AutorizaNotificacionesViaSms: ResolveReportField(
|
AutorizaNotificacionesViaSms: sms,
|
||||||
denuncia,
|
|
||||||
denuncia.Notificacion_Sms,
|
|
||||||
"autorizo recibir notificaciones via sms",
|
|
||||||
"autorizacion notificaciones via sms",
|
|
||||||
"autorizacion para recibir notificaciones via sms",
|
|
||||||
"autoriza notificaciones via sms",
|
|
||||||
"autorizo recibir notificaciones sms",
|
|
||||||
"notificaciones via sms",
|
|
||||||
"sms"),
|
|
||||||
PreferenciaNotificacionSeguimientoDenuncia: JoinDistinct(preferenciaNotificacion, seguimiento));
|
PreferenciaNotificacionSeguimientoDenuncia: JoinDistinct(preferenciaNotificacion, seguimiento));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static GestionaExternalFieldsResponse ToExternalGestionaFields(GestionaComplaintFieldsResponse source)
|
||||||
|
{
|
||||||
|
return new GestionaExternalFieldsResponse(
|
||||||
|
new Dictionary<string, GestionaExternalFieldValue>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
["fechaDenuncia"] = StringField(source.FechaDenuncia?.ToString("O", CultureInfo.InvariantCulture) ?? string.Empty),
|
||||||
|
["numeroDenunciaCanal"] = StringField(source.NumeroDenunciaCanal.ToString(CultureInfo.InvariantCulture)),
|
||||||
|
["aQuienDenuncia"] = StringField(source.AQuienDenuncia),
|
||||||
|
["resumenDenuncia"] = StringField(source.ResumenDenuncia),
|
||||||
|
["fechaHechos"] = StringField(source.FechaHechos),
|
||||||
|
["lugarHechos"] = StringField(source.LugarHechos),
|
||||||
|
["ambitoCompetencias"] = StringField(source.AmbitoCompetencias),
|
||||||
|
["solicitaProteccion"] = StringField(source.SolicitaProteccion),
|
||||||
|
["sexoDenunciante"] = StringField(source.SexoDenunciante),
|
||||||
|
["autorizaRemisionDenuncia"] = StringField(source.AutorizaRemisionDenuncia),
|
||||||
|
["autorizaNotificacionesViaSms"] = StringField(source.AutorizaNotificacionesViaSms),
|
||||||
|
["preferenciaNotificacionSeguimientoDenuncia"] = StringField(source.PreferenciaNotificacionSeguimientoDenuncia)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GestionaExternalFieldValue StringField(string? value)
|
||||||
|
=> new("STRING", value?.Trim() ?? string.Empty);
|
||||||
|
|
||||||
private async Task TryRegisterGestionaHistoryFromComplaintAsync(
|
private async Task TryRegisterGestionaHistoryFromComplaintAsync(
|
||||||
DenunciasGestiona denuncia,
|
DenunciasGestiona denuncia,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -319,6 +333,32 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
private static DateTime? ToNullableDate(DateTime value)
|
private static DateTime? ToNullableDate(DateTime value)
|
||||||
=> value == DateTime.MinValue ? null : value;
|
=> value == DateTime.MinValue ? null : value;
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> GetMissingGestionaFields(GestionaComplaintFieldsResponse response)
|
||||||
|
{
|
||||||
|
var missing = new List<string>();
|
||||||
|
|
||||||
|
AddIfMissing(missing, response.AQuienDenuncia, nameof(response.AQuienDenuncia));
|
||||||
|
AddIfMissing(missing, response.ResumenDenuncia, nameof(response.ResumenDenuncia));
|
||||||
|
AddIfMissing(missing, response.FechaHechos, nameof(response.FechaHechos));
|
||||||
|
AddIfMissing(missing, response.LugarHechos, nameof(response.LugarHechos));
|
||||||
|
AddIfMissing(missing, response.AmbitoCompetencias, nameof(response.AmbitoCompetencias));
|
||||||
|
AddIfMissing(missing, response.SolicitaProteccion, nameof(response.SolicitaProteccion));
|
||||||
|
AddIfMissing(missing, response.SexoDenunciante, nameof(response.SexoDenunciante));
|
||||||
|
AddIfMissing(missing, response.AutorizaRemisionDenuncia, nameof(response.AutorizaRemisionDenuncia));
|
||||||
|
AddIfMissing(missing, response.AutorizaNotificacionesViaSms, nameof(response.AutorizaNotificacionesViaSms));
|
||||||
|
AddIfMissing(missing, response.PreferenciaNotificacionSeguimientoDenuncia, nameof(response.PreferenciaNotificacionSeguimientoDenuncia));
|
||||||
|
|
||||||
|
return missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddIfMissing(List<string> missing, string? value, string fieldName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
missing.Add(fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveFechaHechos(DenunciasGestiona denuncia)
|
private static string ResolveFechaHechos(DenunciasGestiona denuncia)
|
||||||
{
|
{
|
||||||
var literalValue = ResolveReportField(
|
var literalValue = ResolveReportField(
|
||||||
@@ -332,16 +372,125 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
return literalValue;
|
return literalValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var textDate = ExtractDateAfterReportLabel(
|
||||||
|
denuncia.TextoOriginalReport,
|
||||||
|
normalizedLabel => normalizedLabel.Contains("fecha", StringComparison.Ordinal) &&
|
||||||
|
normalizedLabel.Contains("hechos", StringComparison.Ordinal));
|
||||||
|
if (!string.IsNullOrWhiteSpace(textDate))
|
||||||
|
{
|
||||||
|
return textDate;
|
||||||
|
}
|
||||||
|
|
||||||
return denuncia.Fecha_Hechos == DateTime.MinValue
|
return denuncia.Fecha_Hechos == DateTime.MinValue
|
||||||
? string.Empty
|
? string.Empty
|
||||||
: denuncia.Fecha_Hechos.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture);
|
: denuncia.Fecha_Hechos.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ResolveLugarHechos(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
var directValue = ResolveReportField(
|
||||||
|
denuncia,
|
||||||
|
denuncia.Lugar_Hechos,
|
||||||
|
"lugar en el que ocurrieron los hechos que denuncia",
|
||||||
|
"lugar en la que ocurrieron los hechos que denuncia",
|
||||||
|
"lugar de los hechos");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(directValue))
|
||||||
|
{
|
||||||
|
return directValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExtractReportTextValue(
|
||||||
|
denuncia.TextoOriginalReport,
|
||||||
|
normalizedLabel => normalizedLabel.Contains("lugar", StringComparison.Ordinal) &&
|
||||||
|
normalizedLabel.Contains("hechos", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveSeguimientoDenuncia(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
var directValue = ResolveReportField(
|
||||||
|
denuncia,
|
||||||
|
denuncia.SeguimientoOnline,
|
||||||
|
"seguimiento online",
|
||||||
|
"seguimiento de su denuncia");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(directValue))
|
||||||
|
{
|
||||||
|
return directValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReportContainsLabel(denuncia.TextoOriginalReport, "seguimiento online")
|
||||||
|
? "Seguimiento Online"
|
||||||
|
: string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveNotificationPreference(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
var directValue = ResolveReportField(
|
||||||
|
denuncia,
|
||||||
|
denuncia.Notificacion_Preferencia,
|
||||||
|
"preferencia de notificacion",
|
||||||
|
"seleccione su preferencia de notificacion y seguimiento de su denuncia",
|
||||||
|
"preferencia de notificacion y seguimiento de su denuncia",
|
||||||
|
"notificaciones");
|
||||||
|
if (!string.IsNullOrWhiteSpace(directValue))
|
||||||
|
{
|
||||||
|
return directValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
directValue = ResolveReportFieldAllowingLabelValues(
|
||||||
|
denuncia,
|
||||||
|
string.Empty,
|
||||||
|
"preferencia de notificacion",
|
||||||
|
"seleccione su preferencia de notificacion y seguimiento de su denuncia",
|
||||||
|
"preferencia de notificacion y seguimiento de su denuncia");
|
||||||
|
if (!string.IsNullOrWhiteSpace(directValue))
|
||||||
|
{
|
||||||
|
return directValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var electronic = ResolveReportField(
|
||||||
|
denuncia,
|
||||||
|
denuncia.Notificacion_Electronica,
|
||||||
|
"notificaciones electronicas",
|
||||||
|
"notificacion electronica");
|
||||||
|
if (!string.IsNullOrWhiteSpace(electronic) ||
|
||||||
|
ReportContainsLabel(denuncia.TextoOriginalReport, "notificaciones electronicas"))
|
||||||
|
{
|
||||||
|
return "Notificaciones electronicas";
|
||||||
|
}
|
||||||
|
|
||||||
|
var postal = ResolveReportField(
|
||||||
|
denuncia,
|
||||||
|
denuncia.NotificacionPostal,
|
||||||
|
"autorizo recibir notificaciones via correo postal",
|
||||||
|
"notificaciones via correo postal",
|
||||||
|
"correo postal");
|
||||||
|
return string.IsNullOrWhiteSpace(postal) ? string.Empty : "Correo postal";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveSmsNotification(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
return ResolveReportField(
|
||||||
|
denuncia,
|
||||||
|
denuncia.Notificacion_Sms,
|
||||||
|
"autorizo recibir notificaciones via sms",
|
||||||
|
"autorizacion notificaciones via sms",
|
||||||
|
"autorizacion para recibir notificaciones via sms",
|
||||||
|
"autoriza notificaciones via sms",
|
||||||
|
"autorizo recibir notificaciones sms",
|
||||||
|
"notificaciones via sms",
|
||||||
|
"sms");
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveAmbitoCompetencias(DenunciasGestiona denuncia)
|
private static string ResolveAmbitoCompetencias(DenunciasGestiona denuncia)
|
||||||
{
|
{
|
||||||
var directValue = ResolveReportField(
|
var directValue = ResolveReportField(
|
||||||
denuncia,
|
denuncia,
|
||||||
denuncia.Modalidad_Informacion,
|
FirstNonEmpty(denuncia.Modalidad_Informacion, denuncia.Asunto),
|
||||||
|
"asunto",
|
||||||
|
"categoria",
|
||||||
|
"tipo de denuncia",
|
||||||
"ambito de competencias",
|
"ambito de competencias",
|
||||||
"ambito competencial",
|
"ambito competencial",
|
||||||
"ambito",
|
"ambito",
|
||||||
@@ -388,9 +537,41 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ExtractReportTextValue(denuncia.TextoOriginalReport, labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveReportFieldAllowingLabelValues(
|
||||||
|
DenunciasGestiona denuncia,
|
||||||
|
string? currentValue,
|
||||||
|
params string[] candidateLabels)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(currentValue))
|
||||||
|
{
|
||||||
|
return currentValue.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var labels = candidateLabels
|
||||||
|
.Select(NormalizeLabel)
|
||||||
|
.Where(label => !string.IsNullOrWhiteSpace(label))
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
if (labels.Count == 0)
|
||||||
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var field in denuncia.GetCamposFormulario())
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(field.Value) &&
|
||||||
|
labels.Contains(NormalizeLabel(field.Label)))
|
||||||
|
{
|
||||||
|
return field.Value.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExtractReportTextValue(denuncia.TextoOriginalReport, labels, stopAtLikelyLabels: false);
|
||||||
|
}
|
||||||
|
|
||||||
private static string FindReportFieldValue(
|
private static string FindReportFieldValue(
|
||||||
DenunciasGestiona denuncia,
|
DenunciasGestiona denuncia,
|
||||||
Func<string, bool> labelPredicate)
|
Func<string, bool> labelPredicate)
|
||||||
@@ -407,6 +588,176 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ExtractDateAfterReportLabel(string? reportText, Func<string, bool> labelPredicate)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(reportText))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = reportText
|
||||||
|
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||||
|
.Replace('\r', '\n')
|
||||||
|
.Split('\n')
|
||||||
|
.Select(line => line.Trim())
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
for (var index = 0; index < lines.Length; index++)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeLabel(lines[index]);
|
||||||
|
if (!labelPredicate(normalized))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var valueIndex = index + 1; valueIndex < lines.Length && valueIndex <= index + 12; valueIndex++)
|
||||||
|
{
|
||||||
|
var candidate = lines[valueIndex].Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate) || IsReportStructuralLine(candidate))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dateMatch = Regex.Match(
|
||||||
|
candidate,
|
||||||
|
@"\b\d{1,2}/\d{1,2}/\d{4}\b",
|
||||||
|
RegexOptions.CultureInvariant);
|
||||||
|
if (dateMatch.Success)
|
||||||
|
{
|
||||||
|
return dateMatch.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsLikelyReportLabel(candidate))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawMatch = Regex.Match(
|
||||||
|
reportText,
|
||||||
|
@"fecha\s+de\s+los\s+hechos[\s\S]{0,600}?(\b\d{1,2}/\d{1,2}/\d{4}\b)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
return rawMatch.Success ? rawMatch.Groups[1].Value : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractReportTextValue(string? reportText, IReadOnlySet<string> normalizedLabels)
|
||||||
|
=> ExtractReportTextValue(
|
||||||
|
reportText,
|
||||||
|
normalizedLabel => normalizedLabels.Contains(normalizedLabel));
|
||||||
|
|
||||||
|
private static string ExtractReportTextValue(
|
||||||
|
string? reportText,
|
||||||
|
IReadOnlySet<string> normalizedLabels,
|
||||||
|
bool stopAtLikelyLabels)
|
||||||
|
=> ExtractReportTextValue(
|
||||||
|
reportText,
|
||||||
|
normalizedLabel => normalizedLabels.Contains(normalizedLabel),
|
||||||
|
stopAtLikelyLabels);
|
||||||
|
|
||||||
|
private static string ExtractReportTextValue(string? reportText, Func<string, bool> labelPredicate)
|
||||||
|
=> ExtractReportTextValue(reportText, labelPredicate, stopAtLikelyLabels: true);
|
||||||
|
|
||||||
|
private static string ExtractReportTextValue(
|
||||||
|
string? reportText,
|
||||||
|
Func<string, bool> labelPredicate,
|
||||||
|
bool stopAtLikelyLabels)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(reportText))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = reportText
|
||||||
|
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||||
|
.Replace('\r', '\n')
|
||||||
|
.Split('\n')
|
||||||
|
.Select(line => line.Trim())
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
for (var index = 0; index < lines.Length; index++)
|
||||||
|
{
|
||||||
|
if (!labelPredicate(NormalizeLabel(lines[index])))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var valueIndex = index + 1; valueIndex < lines.Length; valueIndex++)
|
||||||
|
{
|
||||||
|
var candidate = lines[valueIndex].Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(candidate) || IsReportStructuralLine(candidate))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopAtLikelyLabels && IsLikelyReportLabel(candidate))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ReportContainsLabel(string? reportText, string label)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(reportText))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedLabel = NormalizeLabel(label);
|
||||||
|
return reportText
|
||||||
|
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||||
|
.Replace('\r', '\n')
|
||||||
|
.Split('\n')
|
||||||
|
.Any(line => string.Equals(NormalizeLabel(line.Trim()), normalizedLabel, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsReportStructuralLine(string value)
|
||||||
|
{
|
||||||
|
if (Regex.IsMatch(value, @"^\d+/\d+$", RegexOptions.CultureInvariant))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.StartsWith("REPORT ", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
value.Equals("[CONFIDENTIAL]", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
value.Equals("{Messages}", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
value.StartsWith("Comments", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
value.StartsWith("De:", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
value.StartsWith("Fecha:", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsLikelyReportLabel(string value)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeLabel(value);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized is "descripcion"
|
||||||
|
or "datos del denunciante"
|
||||||
|
or "condiciones y reglas de uso"
|
||||||
|
or "tratamiento de datos personales"
|
||||||
|
or "preferencias de notificacion"
|
||||||
|
or "notificaciones electronicas" ||
|
||||||
|
normalized.StartsWith("indique ", StringComparison.Ordinal) ||
|
||||||
|
normalized.StartsWith("describa ", StringComparison.Ordinal) ||
|
||||||
|
normalized.StartsWith("autoriza ", StringComparison.Ordinal) ||
|
||||||
|
normalized.StartsWith("autorizacion ", StringComparison.Ordinal) ||
|
||||||
|
normalized.StartsWith("ha denunciado ", StringComparison.Ordinal) ||
|
||||||
|
normalized.StartsWith("solicita ", StringComparison.Ordinal) ||
|
||||||
|
normalized.StartsWith("por favor ", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains(" denuncia", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains(" hechos", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains(" notificacion", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
private static string JoinDistinct(params string[] values)
|
private static string JoinDistinct(params string[] values)
|
||||||
{
|
{
|
||||||
var result = new List<string>();
|
var result = new List<string>();
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ public sealed class InboxController : ControllerBase
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _sessionStore.UpdateSessionAsync(username, session.Id, session.Role, cancellationToken);
|
await _sessionStore.UpdateSessionAsync(username, session.Id, session.Role, session.DpopPrivateKey, cancellationToken);
|
||||||
var stored = await _sessionStore.GetAsync(username, cancellationToken);
|
var stored = await _sessionStore.GetAsync(username, cancellationToken);
|
||||||
return Ok(ToDto(stored));
|
return Ok(ToDto(stored));
|
||||||
}
|
}
|
||||||
@@ -173,13 +173,21 @@ public sealed class InboxController : ControllerBase
|
|||||||
{
|
{
|
||||||
var state = await _trackingService.GetUserStateAsync(username, cancellationToken);
|
var state = await _trackingService.GetUserStateAsync(username, cancellationToken);
|
||||||
var contexts = await _globalLeaksClient.GetContextsAsync(session.SessionId!, cancellationToken);
|
var contexts = await _globalLeaksClient.GetContextsAsync(session.SessionId!, cancellationToken);
|
||||||
var reports = await _globalLeaksClient.GetReportsAsync(session.SessionId!, "all", null, null, cancellationToken, contexts);
|
var reports = await _globalLeaksClient.GetReportsAsync(
|
||||||
|
session.SessionId!,
|
||||||
|
"all",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
cancellationToken,
|
||||||
|
contexts,
|
||||||
|
session.DpopPrivateKey);
|
||||||
var enrichedReports = await _trackingService.RegisterSnapshotAsync(username, reports, cancellationToken);
|
var enrichedReports = await _trackingService.RegisterSnapshotAsync(username, reports, cancellationToken);
|
||||||
var activityReports = await _globalLeaksClient.EnrichReportsWithActivityAsync(
|
var activityReports = await _globalLeaksClient.EnrichReportsWithActivityAsync(
|
||||||
session.SessionId!,
|
session.SessionId!,
|
||||||
enrichedReports,
|
enrichedReports,
|
||||||
state.LastDownloadedReportMomentUtc,
|
state.LastDownloadedReportMomentUtc,
|
||||||
cancellationToken);
|
cancellationToken,
|
||||||
|
session.DpopPrivateKey);
|
||||||
|
|
||||||
return Ok(new InboxSnapshotResponse(contexts, activityReports, state));
|
return Ok(new InboxSnapshotResponse(contexts, activityReports, state));
|
||||||
}
|
}
|
||||||
@@ -229,7 +237,8 @@ public sealed class InboxController : ControllerBase
|
|||||||
session.SessionId!,
|
session.SessionId!,
|
||||||
report.Id,
|
report.Id,
|
||||||
report.LastAccess,
|
report.LastAccess,
|
||||||
cancellationToken);
|
cancellationToken,
|
||||||
|
session.DpopPrivateKey);
|
||||||
}
|
}
|
||||||
catch (GlobalLeaksValidationException ex) when (ex.StatusCode == StatusCodes.Status422UnprocessableEntity ||
|
catch (GlobalLeaksValidationException ex) when (ex.StatusCode == StatusCodes.Status422UnprocessableEntity ||
|
||||||
ex.StatusCode is >= 500 and <= 504)
|
ex.StatusCode is >= 500 and <= 504)
|
||||||
@@ -240,12 +249,16 @@ public sealed class InboxController : ControllerBase
|
|||||||
report.Id);
|
report.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
var reportPackage = await DownloadReportPackageWithRetryAsync(session.SessionId!, report, cancellationToken);
|
var reportPackage = await DownloadReportPackageWithRetryAsync(session, report, cancellationToken);
|
||||||
|
|
||||||
FileDownloadResult? json = null;
|
FileDownloadResult? json = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
json = await _globalLeaksClient.ExportReportJsonAsync(session.SessionId!, report.Id, cancellationToken);
|
json = await _globalLeaksClient.ExportReportJsonAsync(
|
||||||
|
session.SessionId!,
|
||||||
|
report.Id,
|
||||||
|
cancellationToken,
|
||||||
|
session.DpopPrivateKey);
|
||||||
}
|
}
|
||||||
catch (GlobalLeaksValidationException ex) when (ex.StatusCode == 422)
|
catch (GlobalLeaksValidationException ex) when (ex.StatusCode == 422)
|
||||||
{
|
{
|
||||||
@@ -297,7 +310,12 @@ public sealed class InboxController : ControllerBase
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Ok(await _globalLeaksClient.GetReportDetailAsync(session.SessionId!, reportId, lastAccess, cancellationToken));
|
return Ok(await _globalLeaksClient.GetReportDetailAsync(
|
||||||
|
session.SessionId!,
|
||||||
|
reportId,
|
||||||
|
lastAccess,
|
||||||
|
cancellationToken,
|
||||||
|
session.DpopPrivateKey));
|
||||||
}
|
}
|
||||||
catch (GlobalLeaksSessionExpiredException)
|
catch (GlobalLeaksSessionExpiredException)
|
||||||
{
|
{
|
||||||
@@ -318,7 +336,7 @@ public sealed class InboxController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<FileDownloadResult> DownloadReportPackageWithRetryAsync(
|
private async Task<FileDownloadResult> DownloadReportPackageWithRetryAsync(
|
||||||
string sessionId,
|
GlobalLeaksStoredSession session,
|
||||||
ReportDto report,
|
ReportDto report,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -326,7 +344,11 @@ public sealed class InboxController : ControllerBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await _globalLeaksClient.DownloadReportPackageAsync(sessionId, report.Id, cancellationToken);
|
return await _globalLeaksClient.DownloadReportPackageAsync(
|
||||||
|
session.SessionId!,
|
||||||
|
report.Id,
|
||||||
|
cancellationToken,
|
||||||
|
session.DpopPrivateKey);
|
||||||
}
|
}
|
||||||
catch (GlobalLeaksValidationException ex) when (IsExportNotReady(ex) && attempt < ExportRetryDelays.Length)
|
catch (GlobalLeaksValidationException ex) when (IsExportNotReady(ex) && attempt < ExportRetryDelays.Length)
|
||||||
{
|
{
|
||||||
@@ -353,11 +375,37 @@ public sealed class InboxController : ControllerBase
|
|||||||
private static bool IsExportNotReady(GlobalLeaksValidationException ex)
|
private static bool IsExportNotReady(GlobalLeaksValidationException ex)
|
||||||
=> ex.StatusCode is >= 500 and <= 504;
|
=> ex.StatusCode is >= 500 and <= 504;
|
||||||
|
|
||||||
private static ActionResult ToGlobalLeaksApiError(GlobalLeaksValidationException ex, string operation)
|
private ActionResult ToGlobalLeaksApiError(GlobalLeaksValidationException ex, string operation)
|
||||||
{
|
{
|
||||||
if (ex.StatusCode == StatusCodes.Status401Unauthorized ||
|
if (ex.StatusCode == StatusCodes.Status401Unauthorized ||
|
||||||
ex.StatusCode == StatusCodes.Status403Forbidden)
|
ex.StatusCode == StatusCodes.Status403Forbidden)
|
||||||
{
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"GlobalLeaks rechazo la operacion '{Operation}'. Status={StatusCode}. Mensaje={Message}",
|
||||||
|
operation,
|
||||||
|
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(
|
||||||
|
"La sesion de GlobalLeaks no es valida o ha caducado. Renueva el 2FA y vuelve a intentarlo.",
|
||||||
|
SessionExpired: true))
|
||||||
|
{
|
||||||
|
StatusCode = StatusCodes.Status401Unauthorized
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return new ObjectResult(new ApiError(
|
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."))
|
"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."))
|
||||||
{
|
{
|
||||||
@@ -380,6 +428,13 @@ 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("Invalid session", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
message.Contains("session", StringComparison.OrdinalIgnoreCase) && message.Contains("expired", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
private async Task<GlobalLeaksStoredSession?> RequireActiveSessionAsync(string username, CancellationToken cancellationToken)
|
private async Task<GlobalLeaksStoredSession?> RequireActiveSessionAsync(string username, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var session = await _sessionStore.GetAsync(username, cancellationToken);
|
var session = await _sessionStore.GetAsync(username, cancellationToken);
|
||||||
|
|||||||
@@ -121,15 +121,26 @@ public static class GlobalLeaksJsonEnricher
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.TryGetProperty("children", out var children) &&
|
foreach (var property in element.EnumerateObject())
|
||||||
children.ValueKind == JsonValueKind.Array)
|
|
||||||
{
|
{
|
||||||
foreach (var child in children.EnumerateArray())
|
if (property.NameEquals("options"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.Value.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
CollectDefinitions(property.Value, definitions);
|
||||||
|
}
|
||||||
|
else if (property.Value.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var child in property.Value.EnumerateArray())
|
||||||
{
|
{
|
||||||
CollectDefinitions(child, definitions);
|
CollectDefinitions(child, definitions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Dictionary<string, string> ParseOptions(JsonElement element)
|
private static Dictionary<string, string> ParseOptions(JsonElement element)
|
||||||
{
|
{
|
||||||
@@ -163,19 +174,35 @@ public static class GlobalLeaksJsonEnricher
|
|||||||
|
|
||||||
private static string ResolveAnswer(JsonElement answerArray, FieldDefinition definition)
|
private static string ResolveAnswer(JsonElement answerArray, FieldDefinition definition)
|
||||||
{
|
{
|
||||||
|
if (answerArray.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return ResolveAnswerObject(answerArray, definition);
|
||||||
|
}
|
||||||
|
|
||||||
if (answerArray.ValueKind != JsonValueKind.Array)
|
if (answerArray.ValueKind != JsonValueKind.Array)
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return ResolveValue(answerArray, definition.Options);
|
||||||
}
|
}
|
||||||
|
|
||||||
var values = new List<string>();
|
var values = new List<string>();
|
||||||
|
|
||||||
foreach (var answer in answerArray.EnumerateArray())
|
foreach (var answer in answerArray.EnumerateArray())
|
||||||
{
|
{
|
||||||
if (answer.ValueKind != JsonValueKind.Object)
|
var resolved = answer.ValueKind == JsonValueKind.Object
|
||||||
|
? ResolveAnswerObject(answer, definition)
|
||||||
|
: ResolveValue(answer, definition.Options);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolved))
|
||||||
{
|
{
|
||||||
continue;
|
values.Add(resolved);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("; ", values.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveAnswerObject(JsonElement answer, FieldDefinition definition)
|
||||||
|
{
|
||||||
|
var values = new List<string>();
|
||||||
|
|
||||||
if (answer.TryGetProperty("value", out var valueElement))
|
if (answer.TryGetProperty("value", out var valueElement))
|
||||||
{
|
{
|
||||||
@@ -200,7 +227,6 @@ public static class GlobalLeaksJsonEnricher
|
|||||||
values.Add(label);
|
values.Add(label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return string.Join("; ", values.Distinct(StringComparer.OrdinalIgnoreCase));
|
return string.Join("; ", values.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
@@ -213,10 +239,95 @@ public static class GlobalLeaksJsonEnricher
|
|||||||
JsonValueKind.True => "Sí",
|
JsonValueKind.True => "Sí",
|
||||||
JsonValueKind.False => "No",
|
JsonValueKind.False => "No",
|
||||||
JsonValueKind.Number => valueElement.GetRawText(),
|
JsonValueKind.Number => valueElement.GetRawText(),
|
||||||
|
JsonValueKind.Array => ResolveArrayValue(valueElement, options),
|
||||||
|
JsonValueKind.Object => ResolveObjectValue(valueElement, options),
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ResolveArrayValue(JsonElement valueElement, Dictionary<string, string> options)
|
||||||
|
{
|
||||||
|
var values = valueElement
|
||||||
|
.EnumerateArray()
|
||||||
|
.Select(item => ResolveValue(item, options))
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return string.Join("; ", values);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveObjectValue(JsonElement valueElement, Dictionary<string, string> options)
|
||||||
|
{
|
||||||
|
foreach (var propertyName in new[] { "label", "text", "value", "answer", "date", "formatted", "name" })
|
||||||
|
{
|
||||||
|
if (!valueElement.TryGetProperty(propertyName, out var propertyValue))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolved = ResolveValue(propertyValue, options);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolved))
|
||||||
|
{
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryReadDateParts(valueElement, out var dateText))
|
||||||
|
{
|
||||||
|
return dateText;
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new List<string>();
|
||||||
|
foreach (var property in valueElement.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (property.Value.ValueKind is JsonValueKind.True or JsonValueKind.False &&
|
||||||
|
property.Value.GetBoolean() &&
|
||||||
|
options.TryGetValue(property.Name, out var label))
|
||||||
|
{
|
||||||
|
values.Add(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("; ", values.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadDateParts(JsonElement valueElement, out string dateText)
|
||||||
|
{
|
||||||
|
dateText = string.Empty;
|
||||||
|
if (!TryGetIntProperty(valueElement, "day", out var day) ||
|
||||||
|
!TryGetIntProperty(valueElement, "month", out var month) ||
|
||||||
|
!TryGetIntProperty(valueElement, "year", out var year))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dateText = new DateTime(year, month, day).ToString("dd/MM/yyyy", CultureInfo.InvariantCulture);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetIntProperty(JsonElement valueElement, string propertyName, out int value)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
if (!valueElement.TryGetProperty(propertyName, out var property))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return property.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.Number => property.TryGetInt32(out value),
|
||||||
|
JsonValueKind.String => int.TryParse(property.GetString(), CultureInfo.InvariantCulture, out value),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveStringValue(string? rawValue, Dictionary<string, string> options)
|
private static string ResolveStringValue(string? rawValue, Dictionary<string, string> options)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(rawValue))
|
if (string.IsNullOrWhiteSpace(rawValue))
|
||||||
@@ -259,12 +370,13 @@ public static class GlobalLeaksJsonEnricher
|
|||||||
SetIfMissing(() => denuncia.OrganismoDenunciado, value => denuncia.OrganismoDenunciado = value, answers, "por favor indique el organismo o la institucion donde ha denunciado los hechos");
|
SetIfMissing(() => denuncia.OrganismoDenunciado, value => denuncia.OrganismoDenunciado = value, answers, "por favor indique el organismo o la institucion donde ha denunciado los hechos");
|
||||||
SetIfMissing(() => denuncia.SolicitaProteccion, value => denuncia.SolicitaProteccion = value, answers, "solicita medidas concretas de proteccion");
|
SetIfMissing(() => denuncia.SolicitaProteccion, value => denuncia.SolicitaProteccion = value, answers, "solicita medidas concretas de proteccion");
|
||||||
SetIfMissing(() => denuncia.MedidasProteccionSolicitadas, value => denuncia.MedidasProteccionSolicitadas = value, answers, "describa las medidas de proteccion solicitadas");
|
SetIfMissing(() => denuncia.MedidasProteccionSolicitadas, value => denuncia.MedidasProteccionSolicitadas = value, answers, "describa las medidas de proteccion solicitadas");
|
||||||
SetIfMissing(() => denuncia.Lugar_Hechos, value => denuncia.Lugar_Hechos = value, answers, "lugar en la que ocurrieron los hechos que denuncia");
|
SetIfMissing(() => denuncia.Lugar_Hechos, value => denuncia.Lugar_Hechos = value, answers, "lugar en el que ocurrieron los hechos que denuncia", "lugar en la que ocurrieron los hechos que denuncia", "lugar de los hechos");
|
||||||
SetIfMissing(() => denuncia.AutorizaRemision, value => denuncia.AutorizaRemision = value, answers, "autorizacion para remitir su denuncia");
|
SetIfMissing(() => denuncia.AutorizaRemision, value => denuncia.AutorizaRemision = value, answers, "autorizacion para remitir su denuncia");
|
||||||
SetIfMissing(() => denuncia.PreferenciaRemision, value => denuncia.PreferenciaRemision = value, answers, "en tal caso desea que su denuncia se remita anonimizada sin datos personales");
|
SetIfMissing(() => denuncia.PreferenciaRemision, value => denuncia.PreferenciaRemision = value, answers, "en tal caso desea que su denuncia se remita anonimizada sin datos personales");
|
||||||
SetIfMissing(() => denuncia.Notificacion_Preferencia, value => denuncia.Notificacion_Preferencia = value, answers, "seleccione su preferencia de notificacion y seguimiento de su denuncia", "notificaciones");
|
SetIfMissing(() => denuncia.Notificacion_Preferencia, value => denuncia.Notificacion_Preferencia = value, answers, "preferencia de notificacion", "seleccione su preferencia de notificacion y seguimiento de su denuncia", "preferencia de notificacion y seguimiento de su denuncia", "notificaciones");
|
||||||
SetIfMissing(() => denuncia.Notificacion_Electronica, value => denuncia.Notificacion_Electronica = value, answers, "notificaciones electronicas");
|
SetIfMissing(() => denuncia.Notificacion_Electronica, value => denuncia.Notificacion_Electronica = value, answers, "notificaciones electronicas");
|
||||||
SetIfMissing(() => denuncia.SeguimientoOnline, value => denuncia.SeguimientoOnline = value, answers, "seguimiento online");
|
SetIfMissing(() => denuncia.SeguimientoOnline, value => denuncia.SeguimientoOnline = value, answers, "seguimiento online", "seguimiento de su denuncia");
|
||||||
|
SetIfMissing(() => denuncia.Notificacion_Sms, value => denuncia.Notificacion_Sms = value, answers, "autorizo recibir notificaciones via sms", "autorizacion notificaciones via sms", "autoriza notificaciones via sms", "notificaciones via sms", "sms");
|
||||||
SetIfMissing(() => denuncia.NotificacionPostal, value => denuncia.NotificacionPostal = value, answers, "autorizo recibir notificaciones via correo postal");
|
SetIfMissing(() => denuncia.NotificacionPostal, value => denuncia.NotificacionPostal = value, answers, "autorizo recibir notificaciones via correo postal");
|
||||||
SetIfMissing(() => denuncia.Correo_Electronico, value => denuncia.Correo_Electronico = value, answers, "correo electronico", "email");
|
SetIfMissing(() => denuncia.Correo_Electronico, value => denuncia.Correo_Electronico = value, answers, "correo electronico", "email");
|
||||||
SetIfMissing(() => denuncia.Telefono, value => denuncia.Telefono = value, answers, "contacto telefonico", "telefono", "telefono movil");
|
SetIfMissing(() => denuncia.Telefono, value => denuncia.Telefono = value, answers, "contacto telefonico", "telefono", "telefono movil");
|
||||||
@@ -282,8 +394,8 @@ public static class GlobalLeaksJsonEnricher
|
|||||||
SetIfMissing(() => denuncia.Pais, value => denuncia.Pais = value, answers, "pais");
|
SetIfMissing(() => denuncia.Pais, value => denuncia.Pais = value, answers, "pais");
|
||||||
|
|
||||||
if (denuncia.Fecha_Hechos == DateTime.MinValue &&
|
if (denuncia.Fecha_Hechos == DateTime.MinValue &&
|
||||||
TryGetAnswer(answers, out var fechaHechos, "fecha de los hechos que denuncia") &&
|
TryGetAnswer(answers, out var fechaHechos, "fecha de los hechos que denuncia", "fecha de los hechos") &&
|
||||||
DateTime.TryParse(fechaHechos, CultureInfo.CurrentCulture, DateTimeStyles.None, out var parsedDate))
|
TryParseReportDate(fechaHechos, out var parsedDate))
|
||||||
{
|
{
|
||||||
denuncia.Fecha_Hechos = parsedDate;
|
denuncia.Fecha_Hechos = parsedDate;
|
||||||
}
|
}
|
||||||
@@ -371,6 +483,38 @@ public static class GlobalLeaksJsonEnricher
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryParseReportDate(string? value, out DateTime parsedDate)
|
||||||
|
{
|
||||||
|
parsedDate = DateTime.MinValue;
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var culture in new[]
|
||||||
|
{
|
||||||
|
CultureInfo.GetCultureInfo("es-ES"),
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
CultureInfo.CurrentCulture
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (DateTime.TryParse(value, culture, DateTimeStyles.None, out parsedDate))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var format in new[] { "dd/MM/yyyy", "d/M/yyyy", "yyyy-MM-dd" })
|
||||||
|
{
|
||||||
|
if (DateTime.TryParseExact(value, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedDate))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static string Normalize(string? text)
|
private static string Normalize(string? text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
|||||||
@@ -648,9 +648,20 @@ namespace ApiDenuncias.Services
|
|||||||
"application/vnd.gestiona.filter.thirds+json");
|
"application/vnd.gestiona.filter.thirds+json");
|
||||||
|
|
||||||
using var resp = await _http.SendAsync(req);
|
using var resp = await _http.SendAsync(req);
|
||||||
resp.EnsureSuccessStatusCode();
|
|
||||||
var body = await resp.Content.ReadAsStringAsync();
|
var body = await resp.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent ||
|
||||||
|
string.IsNullOrWhiteSpace(body))
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Error BuscarTerceroPorNifAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
|
||||||
|
}
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
if (!doc.RootElement.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array)
|
if (!doc.RootElement.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array)
|
||||||
return default;
|
return default;
|
||||||
@@ -768,8 +779,13 @@ namespace ApiDenuncias.Services
|
|||||||
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent)
|
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent)
|
||||||
return new HashSet<string>(StringComparer.Ordinal);
|
return new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
resp.EnsureSuccessStatusCode();
|
|
||||||
var body = await resp.Content.ReadAsStringAsync();
|
var body = await resp.Content.ReadAsStringAsync();
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Error ObtenerTercerosEnlazadosAsync: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
|
return new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
using var doc = JsonDocument.Parse(body);
|
using var doc = JsonDocument.Parse(body);
|
||||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ public sealed class GlobalLeaksClient
|
|||||||
if (authResponse.IsSuccessStatusCode)
|
if (authResponse.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var authBody = await authResponse.Content.ReadAsStringAsync(cancellationToken);
|
var authBody = await authResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||||
var session = ParseAuthSession(authBody, username);
|
var session = ParseAuthSession(authBody, username, ExportDpopPrivateKey(dpopKey));
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Login GlobalLeaks correcto para {Username}. Rol: {Role}. Tiempo total={ElapsedMs} ms",
|
"Login GlobalLeaks correcto para {Username}. Rol: {Role}. Tiempo total={ElapsedMs} ms",
|
||||||
session.Username,
|
session.Username,
|
||||||
@@ -233,12 +233,17 @@ public sealed class GlobalLeaksClient
|
|||||||
string? dateFrom,
|
string? dateFrom,
|
||||||
string? dateTo,
|
string? dateTo,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
IReadOnlyList<ContextDto>? contexts = null)
|
IReadOnlyList<ContextDto>? contexts = null,
|
||||||
|
string? dpopPrivateKey = null)
|
||||||
{
|
{
|
||||||
filter ??= "all";
|
filter ??= "all";
|
||||||
|
|
||||||
using var reportsRequest = CreateAuthenticatedRequest(HttpMethod.Get, "/api/recipient/rtips", sessionId);
|
using var reportsResponse = await SendAuthenticatedGlRequestAsync(
|
||||||
using var reportsResponse = await SendGlRequestAsync(reportsRequest, cancellationToken);
|
HttpMethod.Get,
|
||||||
|
"/api/recipient/rtips",
|
||||||
|
sessionId,
|
||||||
|
dpopPrivateKey,
|
||||||
|
cancellationToken);
|
||||||
var body = await reportsResponse.Content.ReadAsStringAsync(cancellationToken);
|
var body = await reportsResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||||
var tips = ParseReports(body);
|
var tips = ParseReports(body);
|
||||||
_logger.LogInformation("GlobalLeaks /api/recipient/rtips devolvió {Count} denuncias", tips.Count);
|
_logger.LogInformation("GlobalLeaks /api/recipient/rtips devolvió {Count} denuncias", tips.Count);
|
||||||
@@ -312,7 +317,8 @@ public sealed class GlobalLeaksClient
|
|||||||
string sessionId,
|
string sessionId,
|
||||||
IReadOnlyList<ReportDto> reports,
|
IReadOnlyList<ReportDto> reports,
|
||||||
DateTimeOffset? fallbackReference,
|
DateTimeOffset? fallbackReference,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken,
|
||||||
|
string? dpopPrivateKey = null)
|
||||||
{
|
{
|
||||||
if (reports.Count == 0)
|
if (reports.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -337,7 +343,7 @@ public sealed class GlobalLeaksClient
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var reference = GetActivityReference(item.Report, fallbackReference);
|
var reference = GetActivityReference(item.Report, fallbackReference);
|
||||||
var activity = await AnalyzeReportActivityAsync(sessionId, item.Report, reference, cancellationToken);
|
var activity = await AnalyzeReportActivityAsync(sessionId, item.Report, reference, dpopPrivateKey, cancellationToken);
|
||||||
enriched[item.Index] = ApplyActivity(item.Report, activity);
|
enriched[item.Index] = ApplyActivity(item.Report, activity);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
|
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
|
||||||
@@ -362,16 +368,17 @@ public sealed class GlobalLeaksClient
|
|||||||
string sessionId,
|
string sessionId,
|
||||||
string reportId,
|
string reportId,
|
||||||
string? lastAccess,
|
string? lastAccess,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken,
|
||||||
|
string? dpopPrivateKey = null)
|
||||||
{
|
{
|
||||||
ValidateUuid(reportId);
|
ValidateUuid(reportId);
|
||||||
|
|
||||||
var reference = ParseDate(lastAccess);
|
var reference = ParseDate(lastAccess);
|
||||||
using var document = await ReadReportDetailDocumentAsync(sessionId, reportId, cancellationToken);
|
using var document = await ReadReportDetailDocumentAsync(sessionId, reportId, dpopPrivateKey, cancellationToken);
|
||||||
IReadOnlyList<ReportCommentDto>? comments = null;
|
IReadOnlyList<ReportCommentDto>? comments = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
comments = await GetReportCommentsAsync(sessionId, reportId, reference, defaultNewWhenNoReference: true, cancellationToken);
|
comments = await GetReportCommentsAsync(sessionId, reportId, reference, defaultNewWhenNoReference: true, dpopPrivateKey, cancellationToken);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
|
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
|
||||||
{
|
{
|
||||||
@@ -387,17 +394,16 @@ public sealed class GlobalLeaksClient
|
|||||||
public async Task<FileDownloadResult> DownloadReportPackageAsync(
|
public async Task<FileDownloadResult> DownloadReportPackageAsync(
|
||||||
string sessionId,
|
string sessionId,
|
||||||
string reportId,
|
string reportId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken,
|
||||||
|
string? dpopPrivateKey = null)
|
||||||
{
|
{
|
||||||
ValidateUuid(reportId);
|
ValidateUuid(reportId);
|
||||||
|
|
||||||
using var request = CreateAuthenticatedRequest(
|
using var response = await SendAuthenticatedGlRequestAsync(
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
$"/api/recipient/rtips/{reportId}/export",
|
$"/api/recipient/rtips/{reportId}/export",
|
||||||
sessionId);
|
sessionId,
|
||||||
|
dpopPrivateKey,
|
||||||
using var response = await SendGlRequestAsync(
|
|
||||||
request,
|
|
||||||
cancellationToken,
|
cancellationToken,
|
||||||
HttpCompletionOption.ResponseHeadersRead);
|
HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
@@ -417,12 +423,17 @@ public sealed class GlobalLeaksClient
|
|||||||
public async Task<FileDownloadResult> ExportReportJsonAsync(
|
public async Task<FileDownloadResult> ExportReportJsonAsync(
|
||||||
string sessionId,
|
string sessionId,
|
||||||
string reportId,
|
string reportId,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken,
|
||||||
|
string? dpopPrivateKey = null)
|
||||||
{
|
{
|
||||||
ValidateUuid(reportId);
|
ValidateUuid(reportId);
|
||||||
|
|
||||||
using var request = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}", sessionId);
|
using var response = await SendAuthenticatedGlRequestAsync(
|
||||||
using var response = await SendGlRequestAsync(request, cancellationToken);
|
HttpMethod.Get,
|
||||||
|
$"/api/recipient/rtips/{reportId}",
|
||||||
|
sessionId,
|
||||||
|
dpopPrivateKey,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
|
var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
|
||||||
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||||
@@ -444,10 +455,15 @@ public sealed class GlobalLeaksClient
|
|||||||
private async Task<JsonDocument> ReadReportDetailDocumentAsync(
|
private async Task<JsonDocument> ReadReportDetailDocumentAsync(
|
||||||
string sessionId,
|
string sessionId,
|
||||||
string reportId,
|
string reportId,
|
||||||
|
string? dpopPrivateKey,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var detailRequest = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}", sessionId);
|
using var detailResponse = await SendAuthenticatedGlRequestAsync(
|
||||||
using var detailResponse = await SendGlRequestAsync(detailRequest, cancellationToken);
|
HttpMethod.Get,
|
||||||
|
$"/api/recipient/rtips/{reportId}",
|
||||||
|
sessionId,
|
||||||
|
dpopPrivateKey,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
var contentType = detailResponse.Content.Headers.ContentType?.MediaType ?? string.Empty;
|
var contentType = detailResponse.Content.Headers.ContentType?.MediaType ?? string.Empty;
|
||||||
var content = await detailResponse.Content.ReadAsByteArrayAsync(cancellationToken);
|
var content = await detailResponse.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||||
@@ -466,10 +482,15 @@ public sealed class GlobalLeaksClient
|
|||||||
string reportId,
|
string reportId,
|
||||||
DateTimeOffset? reference,
|
DateTimeOffset? reference,
|
||||||
bool defaultNewWhenNoReference,
|
bool defaultNewWhenNoReference,
|
||||||
|
string? dpopPrivateKey,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var request = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}/comments", sessionId);
|
using var response = await SendAuthenticatedGlRequestAsync(
|
||||||
using var response = await SendGlRequestAsync(request, cancellationToken);
|
HttpMethod.Get,
|
||||||
|
$"/api/recipient/rtips/{reportId}/comments",
|
||||||
|
sessionId,
|
||||||
|
dpopPrivateKey,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
|
var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
|
||||||
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||||
@@ -499,10 +520,11 @@ public sealed class GlobalLeaksClient
|
|||||||
string sessionId,
|
string sessionId,
|
||||||
ReportDto report,
|
ReportDto report,
|
||||||
DateTimeOffset? reference,
|
DateTimeOffset? reference,
|
||||||
|
string? dpopPrivateKey,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var reportId = report.Id;
|
var reportId = report.Id;
|
||||||
using var document = await ReadReportDetailDocumentAsync(sessionId, reportId, cancellationToken);
|
using var document = await ReadReportDetailDocumentAsync(sessionId, reportId, dpopPrivateKey, cancellationToken);
|
||||||
IReadOnlyList<ReportCommentDto> comments;
|
IReadOnlyList<ReportCommentDto> comments;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -511,6 +533,7 @@ public sealed class GlobalLeaksClient
|
|||||||
reportId,
|
reportId,
|
||||||
reference,
|
reference,
|
||||||
defaultNewWhenNoReference: false,
|
defaultNewWhenNoReference: false,
|
||||||
|
dpopPrivateKey,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
|
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
|
||||||
@@ -650,7 +673,16 @@ public sealed class GlobalLeaksClient
|
|||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
var message = $"Error de GlobalLeaks (código {(int)response.StatusCode}).";
|
var body = await ReadBodySafeAsync(response, cancellationToken);
|
||||||
|
_logger.LogWarning(
|
||||||
|
"GlobalLeaks devolvio {StatusCode} en {Method} {Path}. Body={Body}",
|
||||||
|
(int)response.StatusCode,
|
||||||
|
request.Method.Method,
|
||||||
|
request.RequestUri?.OriginalString,
|
||||||
|
string.IsNullOrWhiteSpace(body) ? "(sin cuerpo)" : body);
|
||||||
|
var message = string.IsNullOrWhiteSpace(body)
|
||||||
|
? $"Error de GlobalLeaks (código {(int)response.StatusCode})."
|
||||||
|
: $"Error de GlobalLeaks (código {(int)response.StatusCode}): {body}";
|
||||||
response.Dispose();
|
response.Dispose();
|
||||||
throw new GlobalLeaksValidationException(message, (int)response.StatusCode);
|
throw new GlobalLeaksValidationException(message, (int)response.StatusCode);
|
||||||
}
|
}
|
||||||
@@ -658,6 +690,38 @@ public sealed class GlobalLeaksClient
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> SendAuthenticatedGlRequestAsync(
|
||||||
|
HttpMethod method,
|
||||||
|
string path,
|
||||||
|
string sessionId,
|
||||||
|
string? dpopPrivateKey,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dpopPrivateKey))
|
||||||
|
{
|
||||||
|
using var requestWithoutDpop = CreateAuthenticatedRequest(method, path, sessionId, dpopPrivateKey: null);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static HttpRequestMessage CreateRequest(HttpMethod method, string path)
|
private static HttpRequestMessage CreateRequest(HttpMethod method, string path)
|
||||||
{
|
{
|
||||||
var request = new HttpRequestMessage(method, path)
|
var request = new HttpRequestMessage(method, path)
|
||||||
@@ -671,10 +735,26 @@ public sealed class GlobalLeaksClient
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HttpRequestMessage CreateAuthenticatedRequest(HttpMethod method, string path, string sessionId)
|
private static HttpRequestMessage CreateAuthenticatedRequest(
|
||||||
|
HttpMethod method,
|
||||||
|
string path,
|
||||||
|
string sessionId,
|
||||||
|
string? dpopPrivateKey)
|
||||||
{
|
{
|
||||||
var request = CreateRequest(method, path);
|
var request = CreateRequest(method, path);
|
||||||
request.Headers.Add("X-Session", sessionId);
|
request.Headers.Add("X-Session", sessionId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(dpopPrivateKey))
|
||||||
|
{
|
||||||
|
using var key = ImportDpopPrivateKey(dpopPrivateKey);
|
||||||
|
request.Headers.TryAddWithoutValidation(
|
||||||
|
"DPoP",
|
||||||
|
CreateDpopProof(
|
||||||
|
key,
|
||||||
|
method.Method.ToUpperInvariant(),
|
||||||
|
GetDpopPath(path),
|
||||||
|
ComputeAth(sessionId)));
|
||||||
|
}
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,7 +797,7 @@ public sealed class GlobalLeaksClient
|
|||||||
return Convert.ToBase64String(hash);
|
return Convert.ToBase64String(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string CreateDpopProof(ECDsa privateKey, string method, string path)
|
private static string CreateDpopProof(ECDsa privateKey, string method, string path, string? ath = null)
|
||||||
{
|
{
|
||||||
var publicParameters = privateKey.ExportParameters(includePrivateParameters: false);
|
var publicParameters = privateKey.ExportParameters(includePrivateParameters: false);
|
||||||
var jwk = new Dictionary<string, object?>
|
var jwk = new Dictionary<string, object?>
|
||||||
@@ -742,6 +822,10 @@ public sealed class GlobalLeaksClient
|
|||||||
["htu"] = path,
|
["htu"] = path,
|
||||||
["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
};
|
};
|
||||||
|
if (!string.IsNullOrWhiteSpace(ath))
|
||||||
|
{
|
||||||
|
payload["ath"] = ath;
|
||||||
|
}
|
||||||
|
|
||||||
var encodedHeader = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(header, JsonOptions));
|
var encodedHeader = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(header, JsonOptions));
|
||||||
var encodedPayload = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(payload, JsonOptions));
|
var encodedPayload = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(payload, JsonOptions));
|
||||||
@@ -754,6 +838,25 @@ public sealed class GlobalLeaksClient
|
|||||||
return $"{signingInput}.{Base64UrlEncode(signature)}";
|
return $"{signingInput}.{Base64UrlEncode(signature)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ExportDpopPrivateKey(ECDsa key)
|
||||||
|
=> Convert.ToBase64String(key.ExportECPrivateKey());
|
||||||
|
|
||||||
|
private static ECDsa ImportDpopPrivateKey(string privateKey)
|
||||||
|
{
|
||||||
|
var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||||
|
key.ImportECPrivateKey(Convert.FromBase64String(privateKey), out _);
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeAth(string sessionId)
|
||||||
|
=> Base64UrlEncode(SHA256.HashData(Encoding.UTF8.GetBytes(sessionId)));
|
||||||
|
|
||||||
|
private static string GetDpopPath(string path)
|
||||||
|
{
|
||||||
|
var queryIndex = path.IndexOf('?', StringComparison.Ordinal);
|
||||||
|
return queryIndex < 0 ? path : path[..queryIndex];
|
||||||
|
}
|
||||||
|
|
||||||
private static string Base64UrlEncode(byte[] bytes)
|
private static string Base64UrlEncode(byte[] bytes)
|
||||||
=> Convert.ToBase64String(bytes)
|
=> Convert.ToBase64String(bytes)
|
||||||
.TrimEnd('=')
|
.TrimEnd('=')
|
||||||
@@ -1309,12 +1412,12 @@ public sealed class GlobalLeaksClient
|
|||||||
private static string? ExtractFileName(ContentDispositionHeaderValue? contentDisposition)
|
private static string? ExtractFileName(ContentDispositionHeaderValue? contentDisposition)
|
||||||
=> contentDisposition?.FileNameStar ?? contentDisposition?.FileName?.Trim('"');
|
=> contentDisposition?.FileNameStar ?? contentDisposition?.FileName?.Trim('"');
|
||||||
|
|
||||||
private static GlSession ParseAuthSession(string body, string fallbackUsername)
|
private static GlSession ParseAuthSession(string body, string fallbackUsername, string? dpopPrivateKey)
|
||||||
{
|
{
|
||||||
using var document = JsonDocument.Parse(body);
|
using var document = JsonDocument.Parse(body);
|
||||||
var root = document.RootElement;
|
var root = document.RootElement;
|
||||||
|
|
||||||
var id = GetString(root, "id")
|
var id = GetString(root, "session_id", "sessionId", "session", "sid", "id")
|
||||||
?? throw new GlobalLeaksValidationException("GlobalLeaks no devolvió una sesión válida.", 502);
|
?? throw new GlobalLeaksValidationException("GlobalLeaks no devolvió una sesión válida.", 502);
|
||||||
var username = GetString(root, "username", "name");
|
var username = GetString(root, "username", "name");
|
||||||
if (string.IsNullOrWhiteSpace(username))
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
@@ -1324,7 +1427,7 @@ public sealed class GlobalLeaksClient
|
|||||||
|
|
||||||
var role = GetString(root, "role", "user_role", "userRole");
|
var role = GetString(root, "role", "user_role", "userRole");
|
||||||
|
|
||||||
return new GlSession(id, username, role);
|
return new GlSession(id, username, role, dpopPrivateKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task EnsureSuccessOrThrowAsync(
|
private static async Task EnsureSuccessOrThrowAsync(
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ public sealed class GlobalLeaksSessionStore
|
|||||||
string password,
|
string password,
|
||||||
string sessionId,
|
string sessionId,
|
||||||
string? role,
|
string? role,
|
||||||
|
string? dpopPrivateKey,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var data = new GlobalLeaksStoredSession
|
var data = new GlobalLeaksStoredSession
|
||||||
@@ -62,6 +63,7 @@ public sealed class GlobalLeaksSessionStore
|
|||||||
Password = password,
|
Password = password,
|
||||||
SessionId = sessionId,
|
SessionId = sessionId,
|
||||||
Role = role,
|
Role = role,
|
||||||
|
DpopPrivateKey = dpopPrivateKey,
|
||||||
UpdatedAt = DateTimeOffset.UtcNow,
|
UpdatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ public sealed class GlobalLeaksSessionStore
|
|||||||
string username,
|
string username,
|
||||||
string sessionId,
|
string sessionId,
|
||||||
string? role,
|
string? role,
|
||||||
|
string? dpopPrivateKey,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var current = await GetAsync(username, cancellationToken)
|
var current = await GetAsync(username, cancellationToken)
|
||||||
@@ -79,6 +82,7 @@ public sealed class GlobalLeaksSessionStore
|
|||||||
|
|
||||||
current.SessionId = sessionId;
|
current.SessionId = sessionId;
|
||||||
current.Role = role;
|
current.Role = role;
|
||||||
|
current.DpopPrivateKey = dpopPrivateKey;
|
||||||
current.UpdatedAt = DateTimeOffset.UtcNow;
|
current.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
await WriteAsync(current, cancellationToken);
|
await WriteAsync(current, cancellationToken);
|
||||||
@@ -93,6 +97,7 @@ public sealed class GlobalLeaksSessionStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
current.SessionId = null;
|
current.SessionId = null;
|
||||||
|
current.DpopPrivateKey = null;
|
||||||
current.UpdatedAt = DateTimeOffset.UtcNow;
|
current.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
await WriteAsync(current, cancellationToken);
|
await WriteAsync(current, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,3 +122,10 @@ public sealed record GestionaComplaintFieldsResponse(
|
|||||||
string AutorizaRemisionDenuncia,
|
string AutorizaRemisionDenuncia,
|
||||||
string AutorizaNotificacionesViaSms,
|
string AutorizaNotificacionesViaSms,
|
||||||
string PreferenciaNotificacionSeguimientoDenuncia);
|
string PreferenciaNotificacionSeguimientoDenuncia);
|
||||||
|
|
||||||
|
public sealed record GestionaExternalFieldValue(
|
||||||
|
string Type,
|
||||||
|
string Value);
|
||||||
|
|
||||||
|
public sealed record GestionaExternalFieldsResponse(
|
||||||
|
IReadOnlyDictionary<string, GestionaExternalFieldValue> Data);
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
namespace GestionaDenuncias.Shared.Models;
|
namespace GestionaDenuncias.Shared.Models;
|
||||||
|
|
||||||
public sealed record GlSession(string Id, string Username, string? Role = null);
|
public sealed record GlSession(string Id, string Username, string? Role = null, string? DpopPrivateKey = null);
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ public sealed class GlobalLeaksStoredSession
|
|||||||
public string Password { get; set; } = string.Empty;
|
public string Password { get; set; } = string.Empty;
|
||||||
public string? SessionId { get; set; }
|
public string? SessionId { get; set; }
|
||||||
public string? Role { get; set; }
|
public string? Role { get; set; }
|
||||||
|
public string? DpopPrivateKey { get; set; }
|
||||||
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
public bool HasActiveSession => !string.IsNullOrWhiteSpace(SessionId);
|
public bool HasActiveSession =>
|
||||||
|
!string.IsNullOrWhiteSpace(SessionId) &&
|
||||||
|
!string.IsNullOrWhiteSpace(DpopPrivateKey);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user