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:
2026-07-10 14:00:22 +02:00
parent 1fec0ae0f5
commit 2414fbe80c
10 changed files with 792 additions and 108 deletions

View File

@@ -304,7 +304,7 @@ public sealed class AuthController : ControllerBase
: session.Username.Trim();
_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);
var expiresAtUtc = DateTimeOffset.UtcNow.AddMinutes(Math.Max(5, _jwtOptions.ExpirationMinutes));

View File

@@ -1,5 +1,6 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using ApiDenuncias.Services;
using GestionaDenuncias.Shared.Models;
using Microsoft.AspNetCore.Authorization;
@@ -64,8 +65,9 @@ public sealed class DenunciasController : ControllerBase
return Ok(await _denunciaStore.GetDenunciaByIdAsync(denunciaId, cancellationToken));
}
[AllowAnonymous]
[HttpGet("{denunciaId:int}/gestiona-fields")]
public async Task<ActionResult<GestionaComplaintFieldsResponse>> GetGestionaFields(
public async Task<ActionResult<GestionaExternalFieldsResponse>> GetGestionaFields(
int denunciaId,
CancellationToken cancellationToken)
{
@@ -80,7 +82,19 @@ public sealed class DenunciasController : ControllerBase
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]
@@ -234,18 +248,9 @@ public sealed class DenunciasController : ControllerBase
private static GestionaComplaintFieldsResponse ToGestionaComplaintFields(DenunciasGestiona denuncia)
{
var preferenciaNotificacion = ResolveReportField(
denuncia,
denuncia.Notificacion_Preferencia,
"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");
var preferenciaNotificacion = ResolveNotificationPreference(denuncia);
var seguimiento = ResolveSeguimientoDenuncia(denuncia);
var sms = ResolveSmsNotification(denuncia);
return new GestionaComplaintFieldsResponse(
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, denuncia.Descripcion_Denuncia, "describa su denuncia", "descripcion de la denuncia")),
FechaHechos: ResolveFechaHechos(denuncia),
LugarHechos: 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"),
LugarHechos: ResolveLugarHechos(denuncia),
AmbitoCompetencias: ResolveAmbitoCompetencias(denuncia),
SolicitaProteccion: ResolveReportField(denuncia, denuncia.SolicitaProteccion, "solicita medidas concretas de proteccion", "solicita proteccion"),
SexoDenunciante: ResolveReportField(denuncia, denuncia.Sexo, "sexo"),
AutorizaRemisionDenuncia: ResolveReportField(denuncia, denuncia.AutorizaRemision, "autorizacion para remitir su denuncia", "autoriza remision de la denuncia"),
AutorizaNotificacionesViaSms: 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"),
AutorizaNotificacionesViaSms: sms,
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(
DenunciasGestiona denuncia,
CancellationToken cancellationToken)
@@ -319,6 +333,32 @@ public sealed class DenunciasController : ControllerBase
private static DateTime? ToNullableDate(DateTime 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)
{
var literalValue = ResolveReportField(
@@ -332,16 +372,125 @@ public sealed class DenunciasController : ControllerBase
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
? string.Empty
: 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)
{
var directValue = ResolveReportField(
denuncia,
denuncia.Modalidad_Informacion,
FirstNonEmpty(denuncia.Modalidad_Informacion, denuncia.Asunto),
"asunto",
"categoria",
"tipo de denuncia",
"ambito de competencias",
"ambito competencial",
"ambito",
@@ -388,7 +537,39 @@ public sealed class DenunciasController : ControllerBase
}
}
return string.Empty;
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;
}
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(
@@ -407,6 +588,176 @@ public sealed class DenunciasController : ControllerBase
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)
{
var result = new List<string>();

View File

@@ -131,7 +131,7 @@ public sealed class InboxController : ControllerBase
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);
return Ok(ToDto(stored));
}
@@ -173,13 +173,21 @@ public sealed class InboxController : ControllerBase
{
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 reports = await _globalLeaksClient.GetReportsAsync(
session.SessionId!,
"all",
null,
null,
cancellationToken,
contexts,
session.DpopPrivateKey);
var enrichedReports = await _trackingService.RegisterSnapshotAsync(username, reports, cancellationToken);
var activityReports = await _globalLeaksClient.EnrichReportsWithActivityAsync(
session.SessionId!,
enrichedReports,
state.LastDownloadedReportMomentUtc,
cancellationToken);
cancellationToken,
session.DpopPrivateKey);
return Ok(new InboxSnapshotResponse(contexts, activityReports, state));
}
@@ -229,7 +237,8 @@ public sealed class InboxController : ControllerBase
session.SessionId!,
report.Id,
report.LastAccess,
cancellationToken);
cancellationToken,
session.DpopPrivateKey);
}
catch (GlobalLeaksValidationException ex) when (ex.StatusCode == StatusCodes.Status422UnprocessableEntity ||
ex.StatusCode is >= 500 and <= 504)
@@ -240,12 +249,16 @@ public sealed class InboxController : ControllerBase
report.Id);
}
var reportPackage = await DownloadReportPackageWithRetryAsync(session.SessionId!, report, cancellationToken);
var reportPackage = await DownloadReportPackageWithRetryAsync(session, report, cancellationToken);
FileDownloadResult? json = null;
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)
{
@@ -297,7 +310,12 @@ public sealed class InboxController : ControllerBase
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)
{
@@ -318,7 +336,7 @@ public sealed class InboxController : ControllerBase
}
private async Task<FileDownloadResult> DownloadReportPackageWithRetryAsync(
string sessionId,
GlobalLeaksStoredSession session,
ReportDto report,
CancellationToken cancellationToken)
{
@@ -326,7 +344,11 @@ public sealed class InboxController : ControllerBase
{
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)
{
@@ -353,11 +375,37 @@ public sealed class InboxController : ControllerBase
private static bool IsExportNotReady(GlobalLeaksValidationException ex)
=> 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 ||
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(
"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)
{
var session = await _sessionStore.GetAsync(username, cancellationToken);

View File

@@ -121,12 +121,23 @@ public static class GlobalLeaksJsonEnricher
}
}
if (element.TryGetProperty("children", out var children) &&
children.ValueKind == JsonValueKind.Array)
foreach (var property in element.EnumerateObject())
{
foreach (var child in children.EnumerateArray())
if (property.NameEquals("options"))
{
CollectDefinitions(child, definitions);
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);
}
}
}
}
@@ -163,42 +174,57 @@ public static class GlobalLeaksJsonEnricher
private static string ResolveAnswer(JsonElement answerArray, FieldDefinition definition)
{
if (answerArray.ValueKind == JsonValueKind.Object)
{
return ResolveAnswerObject(answerArray, definition);
}
if (answerArray.ValueKind != JsonValueKind.Array)
{
return string.Empty;
return ResolveValue(answerArray, definition.Options);
}
var values = new List<string>();
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))
{
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))
{
var resolvedValue = ResolveValue(valueElement, definition.Options);
if (!string.IsNullOrWhiteSpace(resolvedValue))
{
values.Add(resolvedValue);
}
}
foreach (var property in answer.EnumerateObject())
{
if (property.NameEquals("value") || property.NameEquals("index") || property.NameEquals("required_status"))
{
continue;
}
if (answer.TryGetProperty("value", out var valueElement))
if (property.Value.ValueKind is JsonValueKind.True or JsonValueKind.False &&
property.Value.GetBoolean() &&
definition.Options.TryGetValue(property.Name, out var label))
{
var resolvedValue = ResolveValue(valueElement, definition.Options);
if (!string.IsNullOrWhiteSpace(resolvedValue))
{
values.Add(resolvedValue);
}
}
foreach (var property in answer.EnumerateObject())
{
if (property.NameEquals("value") || property.NameEquals("index") || property.NameEquals("required_status"))
{
continue;
}
if (property.Value.ValueKind is JsonValueKind.True or JsonValueKind.False &&
property.Value.GetBoolean() &&
definition.Options.TryGetValue(property.Name, out var label))
{
values.Add(label);
}
values.Add(label);
}
}
@@ -213,10 +239,95 @@ public static class GlobalLeaksJsonEnricher
JsonValueKind.True => "Sí",
JsonValueKind.False => "No",
JsonValueKind.Number => valueElement.GetRawText(),
JsonValueKind.Array => ResolveArrayValue(valueElement, options),
JsonValueKind.Object => ResolveObjectValue(valueElement, options),
_ => 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)
{
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.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.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.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.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.Correo_Electronico, value => denuncia.Correo_Electronico = value, answers, "correo electronico", "email");
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");
if (denuncia.Fecha_Hechos == DateTime.MinValue &&
TryGetAnswer(answers, out var fechaHechos, "fecha de los hechos que denuncia") &&
DateTime.TryParse(fechaHechos, CultureInfo.CurrentCulture, DateTimeStyles.None, out var parsedDate))
TryGetAnswer(answers, out var fechaHechos, "fecha de los hechos que denuncia", "fecha de los hechos") &&
TryParseReportDate(fechaHechos, out var parsedDate))
{
denuncia.Fecha_Hechos = parsedDate;
}
@@ -371,6 +483,38 @@ public static class GlobalLeaksJsonEnricher
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)
{
if (string.IsNullOrWhiteSpace(text))

View File

@@ -648,9 +648,20 @@ namespace ApiDenuncias.Services
"application/vnd.gestiona.filter.thirds+json");
using var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
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);
if (!doc.RootElement.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array)
return default;
@@ -768,8 +779,13 @@ namespace ApiDenuncias.Services
if (resp.StatusCode == System.Net.HttpStatusCode.NoContent)
return new HashSet<string>(StringComparer.Ordinal);
resp.EnsureSuccessStatusCode();
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);
var set = new HashSet<string>(StringComparer.Ordinal);

View File

@@ -152,7 +152,7 @@ public sealed class GlobalLeaksClient
if (authResponse.IsSuccessStatusCode)
{
var authBody = await authResponse.Content.ReadAsStringAsync(cancellationToken);
var session = ParseAuthSession(authBody, username);
var session = ParseAuthSession(authBody, username, ExportDpopPrivateKey(dpopKey));
_logger.LogInformation(
"Login GlobalLeaks correcto para {Username}. Rol: {Role}. Tiempo total={ElapsedMs} ms",
session.Username,
@@ -233,12 +233,17 @@ public sealed class GlobalLeaksClient
string? dateFrom,
string? dateTo,
CancellationToken cancellationToken,
IReadOnlyList<ContextDto>? contexts = null)
IReadOnlyList<ContextDto>? contexts = null,
string? dpopPrivateKey = null)
{
filter ??= "all";
using var reportsRequest = CreateAuthenticatedRequest(HttpMethod.Get, "/api/recipient/rtips", sessionId);
using var reportsResponse = await SendGlRequestAsync(reportsRequest, cancellationToken);
using var reportsResponse = await SendAuthenticatedGlRequestAsync(
HttpMethod.Get,
"/api/recipient/rtips",
sessionId,
dpopPrivateKey,
cancellationToken);
var body = await reportsResponse.Content.ReadAsStringAsync(cancellationToken);
var tips = ParseReports(body);
_logger.LogInformation("GlobalLeaks /api/recipient/rtips devolvió {Count} denuncias", tips.Count);
@@ -312,7 +317,8 @@ public sealed class GlobalLeaksClient
string sessionId,
IReadOnlyList<ReportDto> reports,
DateTimeOffset? fallbackReference,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
string? dpopPrivateKey = null)
{
if (reports.Count == 0)
{
@@ -337,7 +343,7 @@ public sealed class GlobalLeaksClient
try
{
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);
}
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
@@ -362,16 +368,17 @@ public sealed class GlobalLeaksClient
string sessionId,
string reportId,
string? lastAccess,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
string? dpopPrivateKey = null)
{
ValidateUuid(reportId);
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;
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)
{
@@ -387,17 +394,16 @@ public sealed class GlobalLeaksClient
public async Task<FileDownloadResult> DownloadReportPackageAsync(
string sessionId,
string reportId,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
string? dpopPrivateKey = null)
{
ValidateUuid(reportId);
using var request = CreateAuthenticatedRequest(
using var response = await SendAuthenticatedGlRequestAsync(
HttpMethod.Get,
$"/api/recipient/rtips/{reportId}/export",
sessionId);
using var response = await SendGlRequestAsync(
request,
sessionId,
dpopPrivateKey,
cancellationToken,
HttpCompletionOption.ResponseHeadersRead);
@@ -417,12 +423,17 @@ public sealed class GlobalLeaksClient
public async Task<FileDownloadResult> ExportReportJsonAsync(
string sessionId,
string reportId,
CancellationToken cancellationToken)
CancellationToken cancellationToken,
string? dpopPrivateKey = null)
{
ValidateUuid(reportId);
using var request = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}", sessionId);
using var response = await SendGlRequestAsync(request, cancellationToken);
using var response = await SendAuthenticatedGlRequestAsync(
HttpMethod.Get,
$"/api/recipient/rtips/{reportId}",
sessionId,
dpopPrivateKey,
cancellationToken);
var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
@@ -444,10 +455,15 @@ public sealed class GlobalLeaksClient
private async Task<JsonDocument> ReadReportDetailDocumentAsync(
string sessionId,
string reportId,
string? dpopPrivateKey,
CancellationToken cancellationToken)
{
using var detailRequest = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}", sessionId);
using var detailResponse = await SendGlRequestAsync(detailRequest, cancellationToken);
using var detailResponse = await SendAuthenticatedGlRequestAsync(
HttpMethod.Get,
$"/api/recipient/rtips/{reportId}",
sessionId,
dpopPrivateKey,
cancellationToken);
var contentType = detailResponse.Content.Headers.ContentType?.MediaType ?? string.Empty;
var content = await detailResponse.Content.ReadAsByteArrayAsync(cancellationToken);
@@ -466,10 +482,15 @@ public sealed class GlobalLeaksClient
string reportId,
DateTimeOffset? reference,
bool defaultNewWhenNoReference,
string? dpopPrivateKey,
CancellationToken cancellationToken)
{
using var request = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}/comments", sessionId);
using var response = await SendGlRequestAsync(request, cancellationToken);
using var response = await SendAuthenticatedGlRequestAsync(
HttpMethod.Get,
$"/api/recipient/rtips/{reportId}/comments",
sessionId,
dpopPrivateKey,
cancellationToken);
var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
@@ -499,10 +520,11 @@ public sealed class GlobalLeaksClient
string sessionId,
ReportDto report,
DateTimeOffset? reference,
string? dpopPrivateKey,
CancellationToken cancellationToken)
{
var reportId = report.Id;
using var document = await ReadReportDetailDocumentAsync(sessionId, reportId, cancellationToken);
using var document = await ReadReportDetailDocumentAsync(sessionId, reportId, dpopPrivateKey, cancellationToken);
IReadOnlyList<ReportCommentDto> comments;
try
{
@@ -511,6 +533,7 @@ public sealed class GlobalLeaksClient
reportId,
reference,
defaultNewWhenNoReference: false,
dpopPrivateKey,
cancellationToken);
}
catch (Exception ex) when (ex is GlobalLeaksValidationException or HttpRequestException or TaskCanceledException)
@@ -650,7 +673,16 @@ public sealed class GlobalLeaksClient
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();
throw new GlobalLeaksValidationException(message, (int)response.StatusCode);
}
@@ -658,6 +690,38 @@ public sealed class GlobalLeaksClient
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)
{
var request = new HttpRequestMessage(method, path)
@@ -671,10 +735,26 @@ public sealed class GlobalLeaksClient
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);
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;
}
@@ -717,7 +797,7 @@ public sealed class GlobalLeaksClient
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 jwk = new Dictionary<string, object?>
@@ -742,6 +822,10 @@ public sealed class GlobalLeaksClient
["htu"] = path,
["iat"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
};
if (!string.IsNullOrWhiteSpace(ath))
{
payload["ath"] = ath;
}
var encodedHeader = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(header, JsonOptions));
var encodedPayload = Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes(payload, JsonOptions));
@@ -754,6 +838,25 @@ public sealed class GlobalLeaksClient
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)
=> Convert.ToBase64String(bytes)
.TrimEnd('=')
@@ -1309,12 +1412,12 @@ public sealed class GlobalLeaksClient
private static string? ExtractFileName(ContentDispositionHeaderValue? contentDisposition)
=> 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);
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);
var username = GetString(root, "username", "name");
if (string.IsNullOrWhiteSpace(username))
@@ -1324,7 +1427,7 @@ public sealed class GlobalLeaksClient
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(

View File

@@ -54,6 +54,7 @@ public sealed class GlobalLeaksSessionStore
string password,
string sessionId,
string? role,
string? dpopPrivateKey,
CancellationToken cancellationToken = default)
{
var data = new GlobalLeaksStoredSession
@@ -62,6 +63,7 @@ public sealed class GlobalLeaksSessionStore
Password = password,
SessionId = sessionId,
Role = role,
DpopPrivateKey = dpopPrivateKey,
UpdatedAt = DateTimeOffset.UtcNow,
};
@@ -72,6 +74,7 @@ public sealed class GlobalLeaksSessionStore
string username,
string sessionId,
string? role,
string? dpopPrivateKey,
CancellationToken cancellationToken = default)
{
var current = await GetAsync(username, cancellationToken)
@@ -79,6 +82,7 @@ public sealed class GlobalLeaksSessionStore
current.SessionId = sessionId;
current.Role = role;
current.DpopPrivateKey = dpopPrivateKey;
current.UpdatedAt = DateTimeOffset.UtcNow;
await WriteAsync(current, cancellationToken);
@@ -93,6 +97,7 @@ public sealed class GlobalLeaksSessionStore
}
current.SessionId = null;
current.DpopPrivateKey = null;
current.UpdatedAt = DateTimeOffset.UtcNow;
await WriteAsync(current, cancellationToken);
}

View File

@@ -122,3 +122,10 @@ public sealed record GestionaComplaintFieldsResponse(
string AutorizaRemisionDenuncia,
string AutorizaNotificacionesViaSms,
string PreferenciaNotificacionSeguimientoDenuncia);
public sealed record GestionaExternalFieldValue(
string Type,
string Value);
public sealed record GestionaExternalFieldsResponse(
IReadOnlyDictionary<string, GestionaExternalFieldValue> Data);

View File

@@ -1,3 +1,3 @@
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);

View File

@@ -6,7 +6,10 @@ public sealed class GlobalLeaksStoredSession
public string Password { get; set; } = string.Empty;
public string? SessionId { get; set; }
public string? Role { get; set; }
public string? DpopPrivateKey { get; set; }
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
public bool HasActiveSession => !string.IsNullOrWhiteSpace(SessionId);
public bool HasActiveSession =>
!string.IsNullOrWhiteSpace(SessionId) &&
!string.IsNullOrWhiteSpace(DpopPrivateKey);
}