diff --git a/Antifraude.Net/Antifraude.Net.sln b/Antifraude.Net/Antifraude.Net.sln
index b0978e4..140d918 100644
--- a/Antifraude.Net/Antifraude.Net.sln
+++ b/Antifraude.Net/Antifraude.Net.sln
@@ -15,36 +15,102 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GestionaDenunciasAN", "Gest
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiDenuncias", "ApiDenuncias\ApiDenuncias.csproj", "{98BF7472-0E7B-4329-A3DC-BB74A942BDAB}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GestionaDenuncias.Shared", "GestionaDenuncias.Shared\GestionaDenuncias.Shared.csproj", "{94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Debug|x64.Build.0 = Debug|Any CPU
+ {B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Debug|x86.Build.0 = Debug|Any CPU
{B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Release|x64.ActiveCfg = Release|Any CPU
+ {B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Release|x64.Build.0 = Release|Any CPU
+ {B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Release|x86.ActiveCfg = Release|Any CPU
+ {B8C0C071-81ED-4265-86F0-169CB5A0C82E}.Release|x86.Build.0 = Release|Any CPU
{ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Debug|x64.Build.0 = Debug|Any CPU
+ {ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Debug|x86.Build.0 = Debug|Any CPU
{ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Release|x64.ActiveCfg = Release|Any CPU
+ {ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Release|x64.Build.0 = Release|Any CPU
+ {ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Release|x86.ActiveCfg = Release|Any CPU
+ {ED8F423B-D059-4A55-AA8F-0122201E4E1A}.Release|x86.Build.0 = Release|Any CPU
{690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Debug|x64.Build.0 = Debug|Any CPU
+ {690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Debug|x86.Build.0 = Debug|Any CPU
{690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Release|Any CPU.ActiveCfg = Release|Any CPU
{690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Release|Any CPU.Build.0 = Release|Any CPU
+ {690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Release|x64.ActiveCfg = Release|Any CPU
+ {690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Release|x64.Build.0 = Release|Any CPU
+ {690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Release|x86.ActiveCfg = Release|Any CPU
+ {690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Release|x86.Build.0 = Release|Any CPU
{063515F3-D202-45DD-91DA-A494FBD005AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{063515F3-D202-45DD-91DA-A494FBD005AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {063515F3-D202-45DD-91DA-A494FBD005AD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {063515F3-D202-45DD-91DA-A494FBD005AD}.Debug|x64.Build.0 = Debug|Any CPU
+ {063515F3-D202-45DD-91DA-A494FBD005AD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {063515F3-D202-45DD-91DA-A494FBD005AD}.Debug|x86.Build.0 = Debug|Any CPU
{063515F3-D202-45DD-91DA-A494FBD005AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{063515F3-D202-45DD-91DA-A494FBD005AD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {063515F3-D202-45DD-91DA-A494FBD005AD}.Release|x64.ActiveCfg = Release|Any CPU
+ {063515F3-D202-45DD-91DA-A494FBD005AD}.Release|x64.Build.0 = Release|Any CPU
+ {063515F3-D202-45DD-91DA-A494FBD005AD}.Release|x86.ActiveCfg = Release|Any CPU
+ {063515F3-D202-45DD-91DA-A494FBD005AD}.Release|x86.Build.0 = Release|Any CPU
{77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Debug|x64.Build.0 = Debug|Any CPU
+ {77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Debug|x86.Build.0 = Debug|Any CPU
{77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Release|x64.ActiveCfg = Release|Any CPU
+ {77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Release|x64.Build.0 = Release|Any CPU
+ {77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Release|x86.ActiveCfg = Release|Any CPU
+ {77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Release|x86.Build.0 = Release|Any CPU
{98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Debug|x64.Build.0 = Debug|Any CPU
+ {98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Debug|x86.Build.0 = Debug|Any CPU
{98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Release|x64.ActiveCfg = Release|Any CPU
+ {98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Release|x64.Build.0 = Release|Any CPU
+ {98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Release|x86.ActiveCfg = Release|Any CPU
+ {98BF7472-0E7B-4329-A3DC-BB74A942BDAB}.Release|x86.Build.0 = Release|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Debug|x64.Build.0 = Debug|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Debug|x86.Build.0 = Debug|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Release|x64.ActiveCfg = Release|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Release|x64.Build.0 = Release|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Release|x86.ActiveCfg = Release|Any CPU
+ {94F25A9A-6084-4F8C-9FF1-F74C6BF83B0E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Antifraude.Net/ApiDenuncias/ApiDenuncias.csproj b/Antifraude.Net/ApiDenuncias/ApiDenuncias.csproj
index d10aa27..accfbb5 100644
--- a/Antifraude.Net/ApiDenuncias/ApiDenuncias.csproj
+++ b/Antifraude.Net/ApiDenuncias/ApiDenuncias.csproj
@@ -7,12 +7,20 @@
+
+
+
+
-
+
+
+
+
+
diff --git a/Antifraude.Net/ApiDenuncias/Configuration/ComplaintStorageOptions.cs b/Antifraude.Net/ApiDenuncias/Configuration/ComplaintStorageOptions.cs
new file mode 100644
index 0000000..73d7ad6
--- /dev/null
+++ b/Antifraude.Net/ApiDenuncias/Configuration/ComplaintStorageOptions.cs
@@ -0,0 +1,18 @@
+namespace ApiDenuncias.Configuration;
+
+public sealed class ComplaintStorageOptions
+{
+ public const string SectionName = "ComplaintStorage";
+
+ public string ConnectionString { get; set; } = string.Empty;
+ public bool AutoCreateSchema { get; set; }
+ public bool UseKeyVault { get; set; } = true;
+ public string HostSecretName { get; set; } = "bbdd-host";
+ public string UserSecretName { get; set; } = "bbdd-user";
+ public string PasswordSecretName { get; set; } = "bbdd-password";
+ public string DatabaseSecretName { get; set; } = "bbdd-name";
+ public string PortSecretName { get; set; } = "bbdd-port";
+ public string SslModeSecretName { get; set; } = "bbdd-ssl-mode";
+ public uint DefaultPort { get; set; } = 3306;
+ public string DefaultSslMode { get; set; } = "Required";
+}
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/GestionaOptions.cs b/Antifraude.Net/ApiDenuncias/Configuration/GestionaOptions.cs
similarity index 53%
rename from Antifraude.Net/GestionaDenunciasAN/Models/GestionaOptions.cs
rename to Antifraude.Net/ApiDenuncias/Configuration/GestionaOptions.cs
index 78d38a1..83a478c 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/GestionaOptions.cs
+++ b/Antifraude.Net/ApiDenuncias/Configuration/GestionaOptions.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models
+namespace ApiDenuncias.Configuration
{
public class GestionaOptions
{
@@ -8,5 +8,10 @@
public string GroupLink { get; set; } = null!;
public string Location { get; set; } = null!;
public string? ExternalProcedureId { get; set; }
+ public string? CircuitTemplateId { get; set; }
+ public string? CircuitSignerStampHref { get; set; }
+ public string? CircuitSignerStampTitle { get; set; }
+ public string? CircuitRecipientGroupHref { get; set; }
+ public string? CircuitVersion { get; set; }
}
}
diff --git a/Antifraude.Net/GestionaDenunciasAN/Configuration/GlobalLeaksOptions.cs b/Antifraude.Net/ApiDenuncias/Configuration/GlobalLeaksOptions.cs
similarity index 68%
rename from Antifraude.Net/GestionaDenunciasAN/Configuration/GlobalLeaksOptions.cs
rename to Antifraude.Net/ApiDenuncias/Configuration/GlobalLeaksOptions.cs
index afc5a59..77ac1f5 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Configuration/GlobalLeaksOptions.cs
+++ b/Antifraude.Net/ApiDenuncias/Configuration/GlobalLeaksOptions.cs
@@ -1,10 +1,12 @@
-namespace GestionaDenunciasAN.Configuration;
+namespace ApiDenuncias.Configuration;
public sealed class GlobalLeaksOptions
{
public const string SectionName = "GlobalLeaks";
public string BaseUrl { get; set; } = "https://prebuzon.antifraudeandalucia.es";
+ public string? HostHeader { get; set; }
+ public bool AllowInvalidCertificate { get; set; }
public int TimeoutSeconds { get; set; } = 120;
public int MaxDownloadBytes { get; set; } = 500 * 1024 * 1024;
}
diff --git a/Antifraude.Net/ApiDenuncias/Configuration/JwtOptions.cs b/Antifraude.Net/ApiDenuncias/Configuration/JwtOptions.cs
index 299b36b..e6870dd 100644
--- a/Antifraude.Net/ApiDenuncias/Configuration/JwtOptions.cs
+++ b/Antifraude.Net/ApiDenuncias/Configuration/JwtOptions.cs
@@ -8,4 +8,5 @@ public sealed class JwtOptions
public string Audience { get; set; } = "GestionaDenunciasAN";
public string SigningKey { get; set; } = string.Empty;
public int ExpirationMinutes { get; set; } = 480;
+ public bool RequireHttpsMetadata { get; set; } = true;
}
diff --git a/Antifraude.Net/ApiDenuncias/Configuration/KeyVaultOptions.cs b/Antifraude.Net/ApiDenuncias/Configuration/KeyVaultOptions.cs
new file mode 100644
index 0000000..3a53654
--- /dev/null
+++ b/Antifraude.Net/ApiDenuncias/Configuration/KeyVaultOptions.cs
@@ -0,0 +1,14 @@
+namespace ApiDenuncias.Configuration;
+
+public sealed class KeyVaultOptions
+{
+ public const string SectionName = "KeyVault";
+
+ public bool Enabled { get; set; } = true;
+
+ public string VaultUrl { get; set; } = string.Empty;
+
+ public string EncryptionKeySecretName { get; set; } = "denuncias-encryption-key";
+
+ public bool AllowLocalEncryptionKeyFallback { get; set; }
+}
diff --git a/Antifraude.Net/ApiDenuncias/Controllers/AuthController.cs b/Antifraude.Net/ApiDenuncias/Controllers/AuthController.cs
index 5f81e9d..631ae31 100644
--- a/Antifraude.Net/ApiDenuncias/Controllers/AuthController.cs
+++ b/Antifraude.Net/ApiDenuncias/Controllers/AuthController.cs
@@ -3,8 +3,8 @@ using System.Security.Claims;
using System.Text;
using System.Text.RegularExpressions;
using ApiDenuncias.Configuration;
-using GestionaDenunciasAN.Models;
-using GestionaDenunciasAN.Services;
+using GestionaDenuncias.Shared.Models;
+using ApiDenuncias.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
diff --git a/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs b/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs
index 857ee62..f2d7ce2 100644
--- a/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs
+++ b/Antifraude.Net/ApiDenuncias/Controllers/DenunciasController.cs
@@ -1,6 +1,6 @@
using ApiDenuncias.Services;
-using GestionaDenunciasAN.Models;
-using GestionaDenunciasAN.Services;
+using GestionaDenuncias.Shared.Models;
+using ApiDenuncias.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
diff --git a/Antifraude.Net/ApiDenuncias/Controllers/GestionaController.cs b/Antifraude.Net/ApiDenuncias/Controllers/GestionaController.cs
index 118022c..9c268de 100644
--- a/Antifraude.Net/ApiDenuncias/Controllers/GestionaController.cs
+++ b/Antifraude.Net/ApiDenuncias/Controllers/GestionaController.cs
@@ -1,5 +1,5 @@
-using GestionaDenunciasAN.Models;
-using GestionaDenunciasAN.Services;
+using GestionaDenuncias.Shared.Models;
+using ApiDenuncias.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
diff --git a/Antifraude.Net/ApiDenuncias/Controllers/InboxController.cs b/Antifraude.Net/ApiDenuncias/Controllers/InboxController.cs
index bf6aee7..560adc3 100644
--- a/Antifraude.Net/ApiDenuncias/Controllers/InboxController.cs
+++ b/Antifraude.Net/ApiDenuncias/Controllers/InboxController.cs
@@ -1,5 +1,5 @@
-using GestionaDenunciasAN.Models;
-using GestionaDenunciasAN.Services;
+using GestionaDenuncias.Shared.Models;
+using ApiDenuncias.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -14,17 +14,20 @@ public sealed class InboxController : ControllerBase
private readonly GlobalLeaksClient _globalLeaksClient;
private readonly DenunciaInboxService _inboxService;
private readonly IInboxTrackingService _trackingService;
+ private readonly ILogger _logger;
public InboxController(
GlobalLeaksSessionStore sessionStore,
GlobalLeaksClient globalLeaksClient,
DenunciaInboxService inboxService,
- IInboxTrackingService trackingService)
+ IInboxTrackingService trackingService,
+ ILogger logger)
{
_sessionStore = sessionStore;
_globalLeaksClient = globalLeaksClient;
_inboxService = inboxService;
_trackingService = trackingService;
+ _logger = logger;
}
[HttpGet("session")]
@@ -67,6 +70,13 @@ public sealed class InboxController : ControllerBase
{
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
}
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "No se ha podido cargar la bandeja GlobalLeaks para {Username}.", username);
+ return StatusCode(
+ StatusCodes.Status500InternalServerError,
+ new ApiError($"No se ha podido cargar la bandeja: {ex.GetType().Name}: {ex.Message}"));
+ }
}
[HttpPost("session/clear")]
@@ -104,6 +114,13 @@ public sealed class InboxController : ControllerBase
{
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
}
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "No se ha podido cargar la bandeja GlobalLeaks para {Username}.", username);
+ return StatusCode(
+ StatusCodes.Status500InternalServerError,
+ new ApiError($"No se ha podido cargar la bandeja: {ex.GetType().Name}: {ex.Message}"));
+ }
}
[HttpPost("reports/{reportId}/import")]
@@ -125,6 +142,8 @@ public sealed class InboxController : ControllerBase
try
{
+ await _trackingService.EnsureReportCanBeImportedByUserAsync(username, report, cancellationToken);
+
var zip = await _globalLeaksClient.DownloadReportZipAsync(session.SessionId!, report.Id, cancellationToken);
FileDownloadResult? json = null;
@@ -158,6 +177,13 @@ public sealed class InboxController : ControllerBase
{
return StatusCode(ex.StatusCode, new ApiError(ex.Message));
}
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "No se ha podido importar la denuncia {ReportId} para {Username}.", reportId, username);
+ return StatusCode(
+ StatusCodes.Status500InternalServerError,
+ new ApiError($"No se ha podido importar la denuncia: {ex.GetType().Name}: {ex.Message}"));
+ }
}
[HttpPost("local/ensure-storage")]
diff --git a/Antifraude.Net/ApiDenuncias/Controllers/TrackingController.cs b/Antifraude.Net/ApiDenuncias/Controllers/TrackingController.cs
index b3c36f1..0810feb 100644
--- a/Antifraude.Net/ApiDenuncias/Controllers/TrackingController.cs
+++ b/Antifraude.Net/ApiDenuncias/Controllers/TrackingController.cs
@@ -1,5 +1,5 @@
-using GestionaDenunciasAN.Models;
-using GestionaDenunciasAN.Services;
+using GestionaDenuncias.Shared.Models;
+using ApiDenuncias.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -34,6 +34,15 @@ public sealed class TrackingController : ControllerBase
return Ok(new { ok = true });
}
+ [HttpPost("import-permission")]
+ public async Task EnsureImportPermission(
+ TrackingImportPermissionRequest request,
+ CancellationToken cancellationToken)
+ {
+ await _trackingService.EnsureReportCanBeImportedByUserAsync(GetUsername(), request.Report, cancellationToken);
+ return Ok(new { ok = true });
+ }
+
private string GetUsername()
=> User.Identity?.Name ?? throw new InvalidOperationException("No hay usuario autenticado.");
}
diff --git a/Antifraude.Net/ApiDenuncias/GlobalUsings.cs b/Antifraude.Net/ApiDenuncias/GlobalUsings.cs
new file mode 100644
index 0000000..80aeb68
--- /dev/null
+++ b/Antifraude.Net/ApiDenuncias/GlobalUsings.cs
@@ -0,0 +1,4 @@
+global using ApiDenuncias.Configuration;
+global using ApiDenuncias.Services;
+global using GestionaDenuncias.Shared.Models;
+global using GestionaDenuncias.Shared.Services;
diff --git a/Antifraude.Net/GestionaDenunciasAN/Helpers/GlobalLeaksJsonEnricher.cs b/Antifraude.Net/ApiDenuncias/Helpers/GlobalLeaksJsonEnricher.cs
similarity index 99%
rename from Antifraude.Net/GestionaDenunciasAN/Helpers/GlobalLeaksJsonEnricher.cs
rename to Antifraude.Net/ApiDenuncias/Helpers/GlobalLeaksJsonEnricher.cs
index b55f332..1388ea9 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Helpers/GlobalLeaksJsonEnricher.cs
+++ b/Antifraude.Net/ApiDenuncias/Helpers/GlobalLeaksJsonEnricher.cs
@@ -1,9 +1,9 @@
using System.Globalization;
using System.Text;
using System.Text.Json;
-using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
-namespace GestionaDenunciasAN.Helpers;
+namespace ApiDenuncias.Helpers;
public static class GlobalLeaksJsonEnricher
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Helpers/ReportParser.cs b/Antifraude.Net/ApiDenuncias/Helpers/ReportParser.cs
similarity index 99%
rename from Antifraude.Net/GestionaDenunciasAN/Helpers/ReportParser.cs
rename to Antifraude.Net/ApiDenuncias/Helpers/ReportParser.cs
index a2b7774..7630e39 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Helpers/ReportParser.cs
+++ b/Antifraude.Net/ApiDenuncias/Helpers/ReportParser.cs
@@ -1,9 +1,9 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
-using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
-namespace GestionaDenunciasAN.Helpers;
+namespace ApiDenuncias.Helpers;
public static class ReportParser
{
diff --git a/Antifraude.Net/ApiDenuncias/Program.cs b/Antifraude.Net/ApiDenuncias/Program.cs
index 0cd2486..9c90734 100644
--- a/Antifraude.Net/ApiDenuncias/Program.cs
+++ b/Antifraude.Net/ApiDenuncias/Program.cs
@@ -2,18 +2,20 @@ using System.Net.Http.Headers;
using System.Text;
using ApiDenuncias.Configuration;
using ApiDenuncias.Services;
-using GestionaDenunciasAN.Configuration;
-using GestionaDenunciasAN.Models;
-using GestionaDenunciasAN.Services;
+using ApiDenuncias.Configuration;
+using GestionaDenuncias.Shared.Models;
+using ApiDenuncias.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
+using GestionaDenuncias.Shared.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure(builder.Configuration.GetSection(JwtOptions.SectionName));
+builder.Services.Configure(builder.Configuration.GetSection(KeyVaultOptions.SectionName));
builder.Services.Configure(builder.Configuration.GetSection("Gestiona"));
builder.Services.Configure(builder.Configuration.GetSection(GlobalLeaksOptions.SectionName));
builder.Services.Configure(builder.Configuration.GetSection(ComplaintStorageOptions.SectionName));
@@ -28,7 +30,9 @@ builder.Services.AddDataProtection()
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddScoped();
+builder.Services.AddSingleton();
builder.Services.AddScoped();
+builder.Services.AddSingleton();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
@@ -50,7 +54,7 @@ builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
- options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
+ options.RequireHttpsMetadata = jwt.RequireHttpsMetadata;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
@@ -62,6 +66,30 @@ builder.Services
IssuerSigningKey = new SymmetricSecurityKey(signingKey),
ClockSkew = TimeSpan.FromMinutes(1)
};
+ options.Events = new JwtBearerEvents
+ {
+ OnAuthenticationFailed = context =>
+ {
+ var logger = context.HttpContext.RequestServices
+ .GetRequiredService()
+ .CreateLogger("ApiDenuncias.Jwt");
+ logger.LogWarning(context.Exception, "JWT no valido en {Path}", context.HttpContext.Request.Path);
+ return Task.CompletedTask;
+ },
+ OnChallenge = context =>
+ {
+ var logger = context.HttpContext.RequestServices
+ .GetRequiredService()
+ .CreateLogger("ApiDenuncias.Jwt");
+ logger.LogWarning(
+ "JWT rechazado en {Path}. Error={Error}. Description={Description}. AuthorizationHeader={HasAuthorizationHeader}",
+ context.HttpContext.Request.Path,
+ context.Error,
+ context.ErrorDescription,
+ context.Request.Headers.ContainsKey("Authorization"));
+ return Task.CompletedTask;
+ }
+ };
});
builder.Services.AddAuthorization();
@@ -82,8 +110,15 @@ app.UseExceptionHandler(errorApp =>
logger.LogError(feature.Error, "Error no controlado en {Path}", context.Request.Path);
}
+ var detailedErrors = context.RequestServices
+ .GetRequiredService()
+ .GetValue("DetailedApiErrors", false);
+ var message = detailedErrors && feature?.Error is not null
+ ? $"La API de denuncias no ha podido completar la operacion: {feature.Error.GetType().Name}: {feature.Error.Message}"
+ : "La API de denuncias no ha podido completar la operacion.";
+
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
- await context.Response.WriteAsJsonAsync(new ApiError("La API de denuncias no ha podido completar la operacion."));
+ await context.Response.WriteAsJsonAsync(new ApiError(message));
});
});
@@ -93,7 +128,10 @@ if (app.Environment.IsDevelopment())
app.UseSwaggerUI();
}
-app.UseHttpsRedirection();
+if (builder.Configuration.GetValue("ForceHttpsRedirection", false))
+{
+ app.UseHttpsRedirection();
+}
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/health", () => Results.Ok(new { status = "ok" })).AllowAnonymous();
diff --git a/Antifraude.Net/GestionaDenunciasAN/Scripts/gestiondenuncias_schema.sql b/Antifraude.Net/ApiDenuncias/Scripts/gestiondenuncias_schema.sql
similarity index 100%
rename from Antifraude.Net/GestionaDenunciasAN/Scripts/gestiondenuncias_schema.sql
rename to Antifraude.Net/ApiDenuncias/Scripts/gestiondenuncias_schema.sql
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/DenunciaInboxService.cs b/Antifraude.Net/ApiDenuncias/Services/DenunciaInboxService.cs
similarity index 68%
rename from Antifraude.Net/GestionaDenunciasAN/Services/DenunciaInboxService.cs
rename to Antifraude.Net/ApiDenuncias/Services/DenunciaInboxService.cs
index e22d9a9..1418984 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/DenunciaInboxService.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/DenunciaInboxService.cs
@@ -1,9 +1,9 @@
-using System.IO.Compression;
+using System.IO.Compression;
using System.Text;
-using GestionaDenunciasAN.Helpers;
-using GestionaDenunciasAN.Models;
+using ApiDenuncias.Helpers;
+using GestionaDenuncias.Shared.Models;
-namespace GestionaDenunciasAN.Services;
+namespace ApiDenuncias.Services;
public sealed class DenunciaInboxService
{
@@ -119,21 +119,39 @@ public sealed class DenunciaInboxService
using var zipStream = new MemoryStream(zipBytes, writable: false);
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read, leaveOpen: false);
- var reportEntry = archive.Entries.FirstOrDefault(entry =>
- string.Equals(NormalizeEntryPath(entry.FullName), "report.txt", StringComparison.OrdinalIgnoreCase));
+ var reportEntry = FindReportEntry(archive);
if (reportEntry is null)
{
- throw new InvalidOperationException("El ZIP no contiene el fichero report.txt.");
+ var entries = archive.Entries
+ .Where(entry => !string.IsNullOrWhiteSpace(entry.Name))
+ .Select(entry => NormalizeEntryPath(entry.FullName))
+ .Take(30)
+ .ToArray();
+
+ throw new InvalidOperationException(
+ entries.Length == 0
+ ? "El ZIP no contiene ficheros."
+ : $"El ZIP no contiene un report reconocible. Ficheros encontrados: {string.Join(", ", entries)}");
}
- var reportText = await ReadEntryTextAsync(reportEntry, cancellationToken);
- var denuncia = ReportParser.ParseReport(reportText);
+ var reportIsPdf = IsPdfEntry(reportEntry);
+ var reportText = reportIsPdf
+ ? string.Empty
+ : await ReadEntryTextAsync(reportEntry, cancellationToken);
+ var denuncia = reportIsPdf
+ ? new DenunciasGestiona()
+ : ReportParser.ParseReport(reportText);
if (!string.IsNullOrWhiteSpace(globalLeaksJson))
{
GlobalLeaksJsonEnricher.Enrich(denuncia, globalLeaksJson);
}
+ else if (reportIsPdf)
+ {
+ throw new InvalidOperationException(
+ "El report viene en PDF y no se ha recibido el JSON de GlobalLeaks necesario para extraer los datos de la denuncia.");
+ }
if (denuncia.Id_Denuncia == 0)
{
@@ -146,6 +164,12 @@ public sealed class DenunciaInboxService
$"No se ha podido determinar el identificador de la denuncia en {sourceName}.");
}
+ if (reportIsPdf)
+ {
+ reportText = BuildSyntheticReportText(denuncia);
+ denuncia.TextoOriginalReport = reportText;
+ }
+
denuncia.ProcedureId = Guid.Empty;
denuncia.GroupId = Guid.Empty;
if (string.IsNullOrWhiteSpace(denuncia.Expediente_Gestiona))
@@ -170,17 +194,17 @@ public sealed class DenunciaInboxService
new(
id_Fichero: 0,
id_Tipo: 1,
- descripcion: "report.txt original",
+ descripcion: IsPdfEntry(reportEntry) ? "report.pdf original" : "report.txt original",
fecha: reportEntry.LastWriteTime.UtcDateTime == DateTime.MinValue
? DateTime.UtcNow
: reportEntry.LastWriteTime.UtcDateTime,
observaciones: "",
id_Denuncia: denunciaId,
- nombreFichero: "report.txt",
+ nombreFichero: IsPdfEntry(reportEntry) ? "report.pdf" : "report.txt",
fichero: await ReadEntryBytesAsync(reportEntry, cancellationToken))
};
- foreach (var entry in archive.Entries.Where(IsSupportedAttachmentEntry))
+ foreach (var entry in archive.Entries.Where(entry => IsSupportedAttachmentEntry(entry) && !IsSameEntry(entry, reportEntry)))
{
files.Add(new FicherosDenuncias(
id_Fichero: 0,
@@ -385,6 +409,44 @@ public sealed class DenunciaInboxService
return IsDirectChildOf(normalized, "files") || IsDirectChildOf(normalized, "files_attached_from_recipients");
}
+ private static ZipArchiveEntry? FindReportEntry(ZipArchive archive)
+ {
+ return archive.Entries.FirstOrDefault(IsReportEntry);
+ }
+
+ private static bool IsReportEntry(ZipArchiveEntry entry)
+ {
+ if (string.IsNullOrWhiteSpace(entry.Name))
+ {
+ return false;
+ }
+
+ var fileName = Path.GetFileName(NormalizeEntryPath(entry.FullName));
+ if (string.Equals(fileName, "report.txt", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+
+ var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
+ var extension = Path.GetExtension(fileName);
+ return (extension.Equals(".txt", StringComparison.OrdinalIgnoreCase) ||
+ extension.Equals(".pdf", StringComparison.OrdinalIgnoreCase)) &&
+ nameWithoutExtension.StartsWith("report", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsPdfEntry(ZipArchiveEntry entry)
+ {
+ return Path.GetExtension(entry.Name).Equals(".pdf", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool IsSameEntry(ZipArchiveEntry left, ZipArchiveEntry right)
+ {
+ return string.Equals(
+ NormalizeEntryPath(left.FullName),
+ NormalizeEntryPath(right.FullName),
+ StringComparison.OrdinalIgnoreCase);
+ }
+
private static bool IsDirectChildOf(string normalizedEntryPath, string rootFolder)
{
if (!normalizedEntryPath.StartsWith(rootFolder + "/", StringComparison.OrdinalIgnoreCase))
@@ -436,5 +498,94 @@ public sealed class DenunciaInboxService
return 0;
}
+
+ private static string BuildSyntheticReportText(DenunciasGestiona denuncia)
+ {
+ var builder = new StringBuilder();
+ builder.AppendLine($"ID: {denuncia.Id_Denuncia}");
+ if (denuncia.Fecha != DateTime.MinValue)
+ {
+ builder.AppendLine($"Fecha: {denuncia.Fecha:O}");
+ }
+
+ AppendMetadata(builder, "Etiqueta", denuncia.Etiqueta);
+ AppendMetadata(builder, "Estado", denuncia.Estado);
+ builder.AppendLine();
+
+ AppendSection(builder, "Datos del denunciante",
+ ("Indique si actúa como persona física o en representación de una persona jurídica.", denuncia.TipoDenunciante),
+ ("Nombre", denuncia.Nombre),
+ ("1º Apellido", denuncia.PrimerApellido),
+ ("2º Apellido", denuncia.SegundoApellido),
+ ("Razón social", denuncia.RazonSocial),
+ ("SEXO", denuncia.Sexo),
+ ("CONTACTO TELEFÓNICO", denuncia.Telefono),
+ ("País de Origen", denuncia.PaisOrigen),
+ ("NIF (DNI, NIE)", denuncia.Dni));
+
+ AppendSection(builder, "Descripción",
+ ("Asunto", denuncia.Asunto),
+ ("¿A quién denuncia?", denuncia.A_Quien_Denuncia),
+ ("Describa su denuncia", denuncia.Descripcion_Denuncia),
+ ("¿Ha denunciado estos hechos ante otras instituciones u órganos?", denuncia.Denunciado_Ante_Inst),
+ ("POR FAVOR. INDIQUE EL ORGANISMO O LA INSTITUCION DONDE HA DENUNCIADO LOS HECHOS", denuncia.OrganismoDenunciado),
+ ("¿Solicita medidas concretas de protección?", denuncia.SolicitaProteccion),
+ ("DESCRIBA LAS MEDIDAS DE PROTECCIÓN SOLICITADAS", denuncia.MedidasProteccionSolicitadas),
+ ("Lugar en el que ocurrieron los hechos que denuncia", denuncia.Lugar_Hechos),
+ ("Fecha de los hechos que denuncia", denuncia.Fecha_Hechos == DateTime.MinValue ? string.Empty : denuncia.Fecha_Hechos.ToString("dd/MM/yyyy")),
+ ("Autorización para remitir su denuncia", denuncia.AutorizaRemision),
+ ("En tal caso, ¿desea que su denuncia se remita anonimizada (sin datos personales)?", denuncia.PreferenciaRemision));
+
+ AppendSection(builder, "Preferencias de notificación",
+ ("Preferencia de notificación", denuncia.Notificacion_Preferencia),
+ ("Notificaciones Electrónicas", denuncia.Notificacion_Electronica),
+ ("Correo electrónico", denuncia.Correo_Electronico),
+ ("Seguimiento Online", denuncia.SeguimientoOnline),
+ ("Autorizo recibir notificaciones vía Correo Postal", denuncia.NotificacionPostal),
+ ("Provincia", denuncia.Provincia),
+ ("Tipo de vía", denuncia.DireccionTipoVia),
+ ("Nombre de la vía", denuncia.Direccion),
+ ("Código Postal", denuncia.CodigoPostal),
+ ("Localidad", denuncia.Municipio),
+ ("Número/Km", denuncia.DireccionNumero),
+ ("Bloque", denuncia.DireccionBloque),
+ ("Escalera", denuncia.DireccionEscalera),
+ ("Planta", denuncia.DireccionPiso),
+ ("Puerta", denuncia.DireccionPuerta),
+ ("Extra", denuncia.DireccionExtra));
+
+ if (!string.IsNullOrWhiteSpace(denuncia.Comments))
+ {
+ builder.AppendLine("{Messages}");
+ builder.AppendLine(denuncia.Comments);
+ }
+
+ return builder.ToString();
+ }
+
+ private static void AppendMetadata(StringBuilder builder, string label, string? value)
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ builder.AppendLine($"{label}: {value}");
+ }
+ }
+
+ private static void AppendSection(StringBuilder builder, string section, params (string Label, string? Value)[] fields)
+ {
+ builder.AppendLine(section);
+ foreach (var (label, value) in fields)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ continue;
+ }
+
+ builder.AppendLine($" {label}");
+ builder.AppendLine($" {value}");
+ }
+
+ builder.AppendLine();
+ }
}
diff --git a/Antifraude.Net/ApiDenuncias/Services/EncryptedDenunciaStore.cs b/Antifraude.Net/ApiDenuncias/Services/EncryptedDenunciaStore.cs
index d8daddf..7e84f5a 100644
--- a/Antifraude.Net/ApiDenuncias/Services/EncryptedDenunciaStore.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/EncryptedDenunciaStore.cs
@@ -1,27 +1,37 @@
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
-using GestionaDenunciasAN.Models;
-using GestionaDenunciasAN.Services;
+using ApiDenuncias.Helpers;
+using GestionaDenuncias.Shared.Models;
+using ApiDenuncias.Services;
using Microsoft.AspNetCore.DataProtection;
namespace ApiDenuncias.Services;
public sealed class EncryptedDenunciaStore : IDenunciaStore
{
- private const string ProtectedStringPrefix = "enc:v1:";
- private static readonly byte[] ProtectedBytesPrefix = Encoding.ASCII.GetBytes("enc:v1:");
+ private const string DataProtectionStringPrefix = "enc:v1:";
+ private const string KeyVaultStringPrefix = "enc:v2:";
+ private static readonly byte[] DataProtectionBytesPrefix = Encoding.ASCII.GetBytes("enc:v1:");
+ private static readonly byte[] KeyVaultBytesPrefix = Encoding.ASCII.GetBytes("enc:v2:");
+ private const int AesGcmNonceSize = 12;
+ private const int AesGcmTagSize = 16;
private static readonly PropertyInfo[] ComplaintProperties = typeof(DenunciasGestiona)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(property => property.CanRead && property.CanWrite)
.ToArray();
private readonly MySqlDenunciaStore _inner;
+ private readonly IEncryptionKeyProvider _encryptionKeyProvider;
private readonly IDataProtector _protector;
- public EncryptedDenunciaStore(MySqlDenunciaStore inner, IDataProtectionProvider dataProtectionProvider)
+ public EncryptedDenunciaStore(
+ MySqlDenunciaStore inner,
+ IEncryptionKeyProvider encryptionKeyProvider,
+ IDataProtectionProvider dataProtectionProvider)
{
_inner = inner;
+ _encryptionKeyProvider = encryptionKeyProvider;
_protector = dataProtectionProvider.CreateProtector("ApiDenuncias.DatabaseSensitiveData.v1");
}
@@ -29,31 +39,47 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore
=> _inner.EnsureSchemaAsync(cancellationToken);
public async Task> GetAllDenunciasAsync(CancellationToken cancellationToken = default)
- => (await _inner.GetAllDenunciasAsync(cancellationToken))
- .Select(UnprotectComplaint)
+ {
+ var key = await _encryptionKeyProvider.GetKeyAsync(cancellationToken);
+ return (await _inner.GetAllDenunciasAsync(cancellationToken))
+ .Select(denuncia => UnprotectComplaint(denuncia, key))
.ToList();
+ }
public async Task> GetAllFicherosAsync(CancellationToken cancellationToken = default)
- => (await _inner.GetAllFicherosAsync(cancellationToken))
- .Select(UnprotectAttachment)
+ {
+ var key = await _encryptionKeyProvider.GetKeyAsync(cancellationToken);
+ return (await _inner.GetAllFicherosAsync(cancellationToken))
+ .Select(fichero => UnprotectAttachment(fichero, key))
.ToList();
+ }
public async Task> GetFicherosByDenunciaAsync(int denunciaId, CancellationToken cancellationToken = default)
- => (await _inner.GetFicherosByDenunciaAsync(denunciaId, cancellationToken))
- .Select(UnprotectAttachment)
+ {
+ var key = await _encryptionKeyProvider.GetKeyAsync(cancellationToken);
+ return (await _inner.GetFicherosByDenunciaAsync(denunciaId, cancellationToken))
+ .Select(fichero => UnprotectAttachment(fichero, key))
.ToList();
+ }
public async Task GetDenunciaByIdAsync(int denunciaId, CancellationToken cancellationToken = default)
{
+ var key = await _encryptionKeyProvider.GetKeyAsync(cancellationToken);
var denuncia = await _inner.GetDenunciaByIdAsync(denunciaId, cancellationToken);
- return denuncia is null ? null : UnprotectComplaint(denuncia);
+ return denuncia is null ? null : UnprotectComplaint(denuncia, key);
}
- public Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default)
- => _inner.UpsertDenunciaAsync(ProtectComplaint(denuncia), cancellationToken);
+ public async Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default)
+ {
+ var key = await _encryptionKeyProvider.GetKeyAsync(cancellationToken);
+ await _inner.UpsertDenunciaAsync(ProtectComplaint(denuncia, key), cancellationToken);
+ }
- public Task UpsertFicherosAsync(IEnumerable ficheros, CancellationToken cancellationToken = default)
- => _inner.UpsertFicherosAsync(ficheros.Select(ProtectAttachment).ToArray(), cancellationToken);
+ public async Task UpsertFicherosAsync(IEnumerable ficheros, CancellationToken cancellationToken = default)
+ {
+ var key = await _encryptionKeyProvider.GetKeyAsync(cancellationToken);
+ await _inner.UpsertFicherosAsync(ficheros.Select(fichero => ProtectAttachment(fichero, key)).ToArray(), cancellationToken);
+ }
public Task MarkFicherosAsUploadedAsync(
int denunciaId,
@@ -62,11 +88,106 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore
CancellationToken cancellationToken = default)
=> _inner.MarkFicherosAsUploadedAsync(denunciaId, fileNames, uploadedAtUtc, cancellationToken);
- private DenunciasGestiona ProtectComplaint(DenunciasGestiona source)
- => TransformComplaint(source, ProtectString);
+ private DenunciasGestiona ProtectComplaint(DenunciasGestiona source, byte[] key)
+ => TransformComplaint(ToPersistentComplaint(source), value => ProtectString(value, key));
- private DenunciasGestiona UnprotectComplaint(DenunciasGestiona source)
- => TransformComplaint(source, UnprotectString);
+ private DenunciasGestiona UnprotectComplaint(DenunciasGestiona source, byte[] key)
+ {
+ var decrypted = TransformComplaint(source, value => UnprotectString(value, key));
+ return RebuildComplaintFromPayload(decrypted);
+ }
+
+ private static DenunciasGestiona ToPersistentComplaint(DenunciasGestiona source)
+ {
+ return new DenunciasGestiona
+ {
+ // Permanentes tecnicos y de trazabilidad.
+ Id_RegistroDenuncia = source.Id_RegistroDenuncia,
+ Id_Denuncia = source.Id_Denuncia,
+ Fecha = source.Fecha,
+ Expediente_Gestiona = source.Expediente_Gestiona,
+ CodigoExpedienteGestiona = source.CodigoExpedienteGestiona,
+ Id_Persona_Gestiona = source.Id_Persona_Gestiona,
+ Etiqueta = source.Etiqueta,
+ Estado = source.Estado,
+ Confidencial = source.Confidencial,
+ EsActualizacion = source.EsActualizacion,
+ ProcedureId = source.ProcedureId,
+ GroupId = source.GroupId,
+ NombreDenuncia = source.NombreDenuncia,
+ EstadoDenuncia = source.EstadoDenuncia,
+ ArchivoElegido = source.ArchivoElegido,
+ FechaSubidaAGestiona = source.FechaSubidaAGestiona,
+ EnGestiona = source.EnGestiona,
+ EnRechazada = source.EnRechazada,
+
+ // Payload temporal cifrado. Los campos funcionales se derivan de aqui al leer.
+ CamposFormularioJson = source.CamposFormularioJson,
+ TextoOriginalReport = source.TextoOriginalReport
+ };
+ }
+
+ private static DenunciasGestiona RebuildComplaintFromPayload(DenunciasGestiona stored)
+ {
+ var rebuilt = TryParseStoredReport(stored) ?? new DenunciasGestiona();
+
+ if (rebuilt.Id_Denuncia == 0)
+ {
+ rebuilt.Id_Denuncia = stored.Id_Denuncia;
+ }
+
+ if (rebuilt.Fecha == DateTime.MinValue)
+ {
+ rebuilt.Fecha = stored.Fecha;
+ }
+
+ if (string.IsNullOrWhiteSpace(rebuilt.CamposFormularioJson))
+ {
+ rebuilt.CamposFormularioJson = stored.CamposFormularioJson;
+ }
+
+ rebuilt.TextoOriginalReport = stored.TextoOriginalReport;
+ ApplyPersistentTechnicalFields(rebuilt, stored);
+ return rebuilt;
+ }
+
+ private static DenunciasGestiona? TryParseStoredReport(DenunciasGestiona stored)
+ {
+ if (string.IsNullOrWhiteSpace(stored.TextoOriginalReport))
+ {
+ return null;
+ }
+
+ try
+ {
+ return ReportParser.ParseReport(stored.TextoOriginalReport);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static void ApplyPersistentTechnicalFields(DenunciasGestiona target, DenunciasGestiona stored)
+ {
+ target.Id_RegistroDenuncia = stored.Id_RegistroDenuncia;
+ target.Id_Denuncia = stored.Id_Denuncia == 0 ? target.Id_Denuncia : stored.Id_Denuncia;
+ target.Expediente_Gestiona = stored.Expediente_Gestiona;
+ target.CodigoExpedienteGestiona = stored.CodigoExpedienteGestiona;
+ target.Id_Persona_Gestiona = stored.Id_Persona_Gestiona;
+ target.Etiqueta = stored.Etiqueta;
+ target.Estado = stored.Estado;
+ target.Confidencial = stored.Confidencial || target.Confidencial;
+ target.EsActualizacion = stored.EsActualizacion;
+ target.ProcedureId = stored.ProcedureId;
+ target.GroupId = stored.GroupId;
+ target.NombreDenuncia = stored.NombreDenuncia;
+ target.EstadoDenuncia = stored.EstadoDenuncia;
+ target.ArchivoElegido = stored.ArchivoElegido;
+ target.FechaSubidaAGestiona = stored.FechaSubidaAGestiona;
+ target.EnGestiona = stored.EnGestiona;
+ target.EnRechazada = stored.EnRechazada;
+ }
private static DenunciasGestiona TransformComplaint(DenunciasGestiona source, Func transformString)
{
@@ -88,7 +209,7 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore
return target;
}
- private FicherosDenuncias ProtectAttachment(FicherosDenuncias source)
+ private FicherosDenuncias ProtectAttachment(FicherosDenuncias source, byte[] key)
{
var content = source.Fichero ?? [];
var hash = string.IsNullOrWhiteSpace(source.ContentSha256)
@@ -99,56 +220,77 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore
{
Id_Fichero = source.Id_Fichero,
Id_Tipo = source.Id_Tipo,
- Descripcion = ProtectString(source.Descripcion ?? string.Empty),
+ Descripcion = ProtectString(source.Descripcion ?? string.Empty, key),
Fecha = source.Fecha,
- Observaciones = ProtectString(source.Observaciones ?? string.Empty),
+ Observaciones = ProtectString(source.Observaciones ?? string.Empty, key),
Id_Denuncia = source.Id_Denuncia,
NombreFichero = source.NombreFichero,
- Fichero = ProtectBytes(content),
+ Fichero = ProtectBytes(content, key),
Subido = source.Subido,
FechaSubida = source.FechaSubida,
ContentSha256 = hash
};
}
- private FicherosDenuncias UnprotectAttachment(FicherosDenuncias source)
+ private FicherosDenuncias UnprotectAttachment(FicherosDenuncias source, byte[] key)
{
return new FicherosDenuncias
{
Id_Fichero = source.Id_Fichero,
Id_Tipo = source.Id_Tipo,
- Descripcion = UnprotectString(source.Descripcion ?? string.Empty),
+ Descripcion = UnprotectString(source.Descripcion ?? string.Empty, key),
Fecha = source.Fecha,
- Observaciones = UnprotectString(source.Observaciones ?? string.Empty),
+ Observaciones = UnprotectString(source.Observaciones ?? string.Empty, key),
Id_Denuncia = source.Id_Denuncia,
NombreFichero = source.NombreFichero,
- Fichero = UnprotectBytes(source.Fichero ?? []),
+ Fichero = UnprotectBytes(source.Fichero ?? [], key),
Subido = source.Subido,
FechaSubida = source.FechaSubida,
ContentSha256 = source.ContentSha256
};
}
- private string ProtectString(string value)
+ private string ProtectString(string value, byte[] key)
{
- if (string.IsNullOrWhiteSpace(value) || value.StartsWith(ProtectedStringPrefix, StringComparison.Ordinal))
+ if (string.IsNullOrWhiteSpace(value) ||
+ value.StartsWith(KeyVaultStringPrefix, StringComparison.Ordinal) ||
+ value.StartsWith(DataProtectionStringPrefix, StringComparison.Ordinal))
{
return value;
}
- return ProtectedStringPrefix + _protector.Protect(value);
+ var encrypted = EncryptBytes(Encoding.UTF8.GetBytes(value), key);
+ return KeyVaultStringPrefix + Convert.ToBase64String(encrypted);
}
- private string UnprotectString(string value)
+ private string UnprotectString(string value, byte[] key)
{
- if (string.IsNullOrWhiteSpace(value) || !value.StartsWith(ProtectedStringPrefix, StringComparison.Ordinal))
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return value;
+ }
+
+ if (value.StartsWith(KeyVaultStringPrefix, StringComparison.Ordinal))
+ {
+ try
+ {
+ var encrypted = Convert.FromBase64String(value[KeyVaultStringPrefix.Length..]);
+ return Encoding.UTF8.GetString(DecryptBytes(encrypted, key));
+ }
+ catch
+ {
+ return value;
+ }
+ }
+
+ if (!value.StartsWith(DataProtectionStringPrefix, StringComparison.Ordinal))
{
return value;
}
try
{
- return _protector.Unprotect(value[ProtectedStringPrefix.Length..]);
+ return _protector.Unprotect(value[DataProtectionStringPrefix.Length..]);
}
catch
{
@@ -156,28 +298,48 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore
}
}
- private byte[] ProtectBytes(byte[] value)
+ private byte[] ProtectBytes(byte[] value, byte[] key)
{
- if (value.Length == 0 || StartsWith(value, ProtectedBytesPrefix))
+ if (value.Length == 0 ||
+ StartsWith(value, KeyVaultBytesPrefix) ||
+ StartsWith(value, DataProtectionBytesPrefix))
{
return value;
}
- var protectedBytes = _protector.Protect(value);
- var base64Bytes = Encoding.ASCII.GetBytes(Convert.ToBase64String(protectedBytes));
- return [.. ProtectedBytesPrefix, .. base64Bytes];
+ var encrypted = EncryptBytes(value, key);
+ var base64Bytes = Encoding.ASCII.GetBytes(Convert.ToBase64String(encrypted));
+ return [.. KeyVaultBytesPrefix, .. base64Bytes];
}
- private byte[] UnprotectBytes(byte[] value)
+ private byte[] UnprotectBytes(byte[] value, byte[] key)
{
- if (value.Length == 0 || !StartsWith(value, ProtectedBytesPrefix))
+ if (value.Length == 0)
+ {
+ return value;
+ }
+
+ if (StartsWith(value, KeyVaultBytesPrefix))
+ {
+ try
+ {
+ var base64 = Encoding.ASCII.GetString(value, KeyVaultBytesPrefix.Length, value.Length - KeyVaultBytesPrefix.Length);
+ return DecryptBytes(Convert.FromBase64String(base64), key);
+ }
+ catch
+ {
+ return value;
+ }
+ }
+
+ if (!StartsWith(value, DataProtectionBytesPrefix))
{
return value;
}
try
{
- var base64 = Encoding.ASCII.GetString(value, ProtectedBytesPrefix.Length, value.Length - ProtectedBytesPrefix.Length);
+ var base64 = Encoding.ASCII.GetString(value, DataProtectionBytesPrefix.Length, value.Length - DataProtectionBytesPrefix.Length);
return _protector.Unprotect(Convert.FromBase64String(base64));
}
catch
@@ -186,6 +348,36 @@ public sealed class EncryptedDenunciaStore : IDenunciaStore
}
}
+ private static byte[] EncryptBytes(byte[] plainBytes, byte[] key)
+ {
+ var nonce = RandomNumberGenerator.GetBytes(AesGcmNonceSize);
+ var cipherBytes = new byte[plainBytes.Length];
+ var tag = new byte[AesGcmTagSize];
+
+ using var aes = new AesGcm(key, AesGcmTagSize);
+ aes.Encrypt(nonce, plainBytes, cipherBytes, tag);
+
+ return [.. nonce, .. tag, .. cipherBytes];
+ }
+
+ private static byte[] DecryptBytes(byte[] encryptedBytes, byte[] key)
+ {
+ if (encryptedBytes.Length < AesGcmNonceSize + AesGcmTagSize)
+ {
+ throw new CryptographicException("Payload cifrado invalido.");
+ }
+
+ var nonce = encryptedBytes.AsSpan(0, AesGcmNonceSize);
+ var tag = encryptedBytes.AsSpan(AesGcmNonceSize, AesGcmTagSize);
+ var cipherBytes = encryptedBytes.AsSpan(AesGcmNonceSize + AesGcmTagSize);
+ var plainBytes = new byte[cipherBytes.Length];
+
+ using var aes = new AesGcm(key, AesGcmTagSize);
+ aes.Decrypt(nonce, cipherBytes, tag, plainBytes);
+
+ return plainBytes;
+ }
+
private static bool StartsWith(byte[] value, byte[] prefix)
{
if (value.Length < prefix.Length)
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/GestionaDocumentWorkflowService.cs b/Antifraude.Net/ApiDenuncias/Services/GestionaDocumentWorkflowService.cs
similarity index 85%
rename from Antifraude.Net/GestionaDenunciasAN/Services/GestionaDocumentWorkflowService.cs
rename to Antifraude.Net/ApiDenuncias/Services/GestionaDocumentWorkflowService.cs
index 4e56b96..0b97d5d 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/GestionaDocumentWorkflowService.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/GestionaDocumentWorkflowService.cs
@@ -5,7 +5,7 @@ using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Extensions.Logging;
-namespace GestionaDenunciasAN.Services;
+namespace ApiDenuncias.Services;
public sealed class GestionaDocumentWorkflowService
{
@@ -53,11 +53,9 @@ public sealed class GestionaDocumentWorkflowService
using var metaReq = new HttpRequestMessage(HttpMethod.Post, documentsTargetUrl);
metaReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", GestionaAccessToken);
+ metaReq.Headers.TryAddWithoutValidation("Prefer", "return=minimal");
metaReq.Headers.Accept.Clear();
- metaReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.file-document+json")
- {
- Parameters = { new NameValueHeaderValue("version", "4") }
- });
+ metaReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
metaReq.Content = new StringContent(metaJson, Encoding.UTF8);
metaReq.Content.Headers.ContentType =
@@ -94,7 +92,7 @@ public sealed class GestionaDocumentWorkflowService
}
catch
{
- // Fallback a busqueda por nombre.
+ // Si Gestiona no devuelve un JSON valido, intentamos con la cabecera Location.
}
}
@@ -104,20 +102,22 @@ public sealed class GestionaDocumentWorkflowService
return location!;
}
- var found = await BuscarDocumentoEnExpedientePorNombreAsync(documentsTargetUrl, fileName);
- if (!string.IsNullOrWhiteSpace(found))
- {
- return found!;
- }
-
throw new InvalidOperationException("No se pudo obtener la URL del documento creado en Gestiona.");
}
public async Task TramitarDocumentoAsync(string documentUrl, string assignedGroupHref, int? complaintId = null)
{
var docUrlAbs = EnsureAbsoluteGestionaUrl(documentUrl, GestionaApiBase);
- var template = await ObtenerTemplateCircuitoFirmaAsync(docUrlAbs);
- var payload = BuildCircuitPayloadFromTemplate(template, assignedGroupHref, complaintId);
+ var payload = BuildConfiguredCircuitPayload(docUrlAbs, assignedGroupHref, complaintId);
+ string? templateNameForLog = "configurada";
+ string? templateHrefForLog = GetConfiguredTemplateHref(docUrlAbs);
+
+ if (payload is null)
+ {
+ throw new InvalidOperationException(
+ "Faltan Gestiona:CircuitTemplateId o Gestiona:CircuitSignerStampHref. No se listan plantillas para evitar campos deprecated.");
+ }
+
var json = payload.ToJsonString(new JsonSerializerOptions(JsonSerializerDefaults.Web));
using var req = new HttpRequestMessage(HttpMethod.Post, $"{docUrlAbs.TrimEnd('/')}/circuit");
@@ -134,8 +134,8 @@ public sealed class GestionaDocumentWorkflowService
_logger.LogError(
"Fallo al tramitar documento {DocumentUrl} con plantilla {TemplateName} ({TemplateHref}). Status: {StatusCode}. Body: {Body}",
docUrlAbs,
- template.Name ?? "(sin nombre)",
- template.Href,
+ templateNameForLog,
+ templateHrefForLog,
(int)resp.StatusCode,
body);
@@ -146,11 +146,67 @@ public sealed class GestionaDocumentWorkflowService
_logger.LogInformation(
"Documento {DocumentUrl} enviado a circuito {TemplateName} ({TemplateHref}) para denuncia {ComplaintId}.",
docUrlAbs,
- template.Name ?? "(sin nombre)",
- template.Href,
+ templateNameForLog,
+ templateHrefForLog,
complaintId);
}
+ private JsonObject? BuildConfiguredCircuitPayload(string documentUrl, string assignedGroupHref, int? complaintId)
+ {
+ _ = assignedGroupHref;
+ _ = complaintId;
+
+ var templateHref = GetConfiguredTemplateHref(documentUrl);
+ var signerHref = _configuration["Gestiona:CircuitSignerStampHref"];
+ if (string.IsNullOrWhiteSpace(templateHref) || string.IsNullOrWhiteSpace(signerHref))
+ {
+ return null;
+ }
+
+ var payload = new JsonObject
+ {
+ ["block_edit"] = true,
+ ["send_alerts"] = true,
+ ["version"] = _configuration["Gestiona:CircuitVersion"] ?? "2",
+ ["signers"] = new JsonArray
+ {
+ JsonSerializer.SerializeToNode(new
+ {
+ rel = "signer-stamp",
+ href = signerHref,
+ title = _configuration["Gestiona:CircuitSignerStampTitle"] ?? "oaaf-complaints-tramit"
+ })
+ },
+ ["links"] = new JsonArray
+ {
+ JsonSerializer.SerializeToNode(new { rel = "self", href = templateHref })
+ }
+ };
+
+ var recipientGroupHref = _configuration["Gestiona:CircuitRecipientGroupHref"];
+ if (!string.IsNullOrWhiteSpace(recipientGroupHref))
+ {
+ payload["recipients"] = new JsonArray
+ {
+ JsonSerializer.SerializeToNode(new
+ {
+ rel = "group",
+ href = recipientGroupHref
+ })
+ };
+ }
+
+ return payload;
+ }
+
+ private string? GetConfiguredTemplateHref(string documentUrl)
+ {
+ var templateId = _configuration["Gestiona:CircuitTemplateId"];
+ return string.IsNullOrWhiteSpace(templateId)
+ ? null
+ : $"{documentUrl.TrimEnd('/')}/circuit/templates/{templateId.Trim()}";
+ }
+
private HttpClient CreateRawHttp() => _httpClientFactory.CreateClient();
private async Task CreateUploadAsync(byte[] contentBytes, string fileName)
@@ -196,55 +252,6 @@ public sealed class GestionaDocumentWorkflowService
return uploadUri;
}
- private async Task BuscarDocumentoEnExpedientePorNombreAsync(string documentsTargetUrl, string fileName)
- {
- using var req = new HttpRequestMessage(HttpMethod.Get, documentsTargetUrl);
- req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", GestionaAccessToken);
- req.Headers.TryAddWithoutValidation("Accept", "*/*");
-
- using var resp = await CreateRawHttp().SendAsync(req);
- var body = await resp.Content.ReadAsStringAsync();
- if (!resp.IsSuccessStatusCode)
- {
- throw new InvalidOperationException(
- $"BuscarDocumentoEnExpedientePorNombreAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
- }
-
- using var doc = JsonDocument.Parse(body);
- if (!doc.RootElement.TryGetProperty("content", out var content) || content.ValueKind != JsonValueKind.Array)
- {
- return null;
- }
-
- var items = content.EnumerateArray().ToList();
- for (var idx = items.Count - 1; idx >= 0; idx--)
- {
- var item = items[idx];
- var name = item.TryGetProperty("name", out var pName) ? pName.GetString() : null;
- if (!string.Equals(name, fileName, StringComparison.OrdinalIgnoreCase))
- {
- continue;
- }
-
- if (item.TryGetProperty("links", out var links) && links.ValueKind == JsonValueKind.Array)
- {
- foreach (var link in links.EnumerateArray())
- {
- var rel = link.TryGetProperty("rel", out var pRel) ? pRel.GetString() : null;
- var href = link.TryGetProperty("href", out var pHref) ? pHref.GetString() : null;
- if (string.Equals(rel, "self", StringComparison.OrdinalIgnoreCase) &&
- !string.IsNullOrWhiteSpace(href) &&
- href!.Contains("/documents/", StringComparison.OrdinalIgnoreCase))
- {
- return href;
- }
- }
- }
- }
-
- return null;
- }
-
private static string ResolveDocumentsContainerUrl(string url)
{
var normalized = url.TrimEnd('/');
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/GestionaService.cs b/Antifraude.Net/ApiDenuncias/Services/GestionaService.cs
similarity index 97%
rename from Antifraude.Net/GestionaDenunciasAN/Services/GestionaService.cs
rename to Antifraude.Net/ApiDenuncias/Services/GestionaService.cs
index fd90895..e1b6f2f 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/GestionaService.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/GestionaService.cs
@@ -1,4 +1,4 @@
-using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
@@ -13,7 +13,7 @@ using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
-namespace GestionaDenunciasAN.Services
+namespace ApiDenuncias.Services
{
public class GestionaService : IGestionaService
{
@@ -58,7 +58,7 @@ namespace GestionaDenunciasAN.Services
return null;
}
- // Reemplaza este helper si quieres controlar la versión en Accept:
+ // Reemplaza este helper si quieres controlar la versin en Accept:
private void AddTokenAndAccept(HttpRequestMessage req, string mediaType, string? version = null)
{
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
@@ -80,7 +80,7 @@ namespace GestionaDenunciasAN.Services
{
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
req.Headers.Accept.Clear();
- req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.files-page+json"));
+ req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
@@ -102,7 +102,7 @@ namespace GestionaDenunciasAN.Services
var url = await ResolveExternalProcedureCreateFileUrlAsync(effectiveProcedureId);
using var req = new HttpRequestMessage(HttpMethod.Post, url);
req.Headers.Accept.Clear();
- req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.file-opening+json"));
+ req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
using var resp = await _http.SendAsync(req);
@@ -113,7 +113,7 @@ namespace GestionaDenunciasAN.Services
using var doc = JsonDocument.Parse(body);
var fileUrl = GetLinkHref(doc.RootElement, "file")
?? throw new InvalidOperationException("CreateFileAsync: Gestiona no ha devuelto link 'file'.");
- var fileOpenUrl = GetLinkHref(doc.RootElement, "file-open") ?? resp.Headers.Location?.ToString();
+ var fileOpenUrl = GetLinkHref(doc.RootElement, "file-open");
return new GestionaCreateFileResponse(fileUrl, fileOpenUrl);
}
@@ -208,7 +208,7 @@ namespace GestionaDenunciasAN.Services
content.Headers.ContentType!.Parameters.Add(new NameValueHeaderValue("version", "1"));
using var req = new HttpRequestMessage(HttpMethod.Post, url) { Content = content };
- AddTokenAndAccept(req, "application/vnd.gestiona.file+json", "2");
+ AddTokenAndAccept(req, "application/json");
using var resp = await _http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
@@ -228,7 +228,7 @@ namespace GestionaDenunciasAN.Services
{
Content = content
};
- AddTokenAndAccept(req, "application/vnd.gestiona.file-folder+json");
+ AddTokenAndAccept(req, "application/json");
using var resp = await _http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
@@ -256,7 +256,7 @@ namespace GestionaDenunciasAN.Services
throw new InvalidOperationException($"CreateUpload (POST): {(int)createResp.StatusCode} {createResp.StatusCode}\n{createBody}");
var uploadUri = createResp.Headers.Location?.ToString()
- ?? throw new InvalidOperationException("No se devolvió Location en /rest/uploads");
+ ?? throw new InvalidOperationException("No se devolvi Location en /rest/uploads");
string md5Hex;
using (var md5 = MD5.Create())
@@ -310,8 +310,7 @@ namespace GestionaDenunciasAN.Services
{ Content = metaContent };
metaReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", _opts.AccessToken);
metaReq.Headers.Accept.Clear();
- metaReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.file-document+json")
- { Parameters = { new NameValueHeaderValue("version", "4") } });
+ metaReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using var metaResp = await _http.SendAsync(metaReq);
var body = await metaResp.Content.ReadAsStringAsync();
@@ -387,7 +386,7 @@ namespace GestionaDenunciasAN.Services
if (thirdParty.IsLegalEntity)
{
if (string.IsNullOrWhiteSpace(thirdParty.BusinessName))
- throw new ArgumentException("La razón social es obligatoria para terceros jurídicos.", nameof(thirdParty));
+ throw new ArgumentException("La razn social es obligatoria para terceros jurdicos.", nameof(thirdParty));
}
else
{
@@ -562,7 +561,7 @@ namespace GestionaDenunciasAN.Services
};
}
- // --- CONSULTAS DE EXPEDIENTES (sin recorrer histórico paginado) ---
+ // --- CONSULTAS DE EXPEDIENTES (sin recorrer histrico paginado) ---
private async Task GetFilesAsync(object? filter = null)
{
@@ -613,7 +612,7 @@ namespace GestionaDenunciasAN.Services
}
///
- /// Devuelve el JSON crudo de /rest/files acumulando hasta maxPages páginas.
+ /// Devuelve el JSON crudo de /rest/files acumulando hasta maxPages pginas.
///
public async Task ListarExpedientesJsonAsyncBasico(int maxPages = 1)
{
@@ -911,7 +910,7 @@ namespace GestionaDenunciasAN.Services
using var resp = await _http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
- throw new InvalidOperationException($"Error actualizando dirección del tercero: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
+ throw new InvalidOperationException($"Error actualizando direccin del tercero: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{body}");
}
private async Task ThirdHasAddressesAsync(string thirdSelfHref)
@@ -1161,7 +1160,7 @@ namespace GestionaDenunciasAN.Services
return value switch
{
"" => "ESP",
- "es" or "esp" or "espana" or "españa" or "spain" => "ESP",
+ "es" or "esp" or "espana" or "espaa" or "spain" => "ESP",
"prt" or "pt" or "portugal" => "PRT",
_ when country is { Length: >= 3 } => country.Trim().ToUpperInvariant()[..3],
_ => "ESP",
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksClient.cs b/Antifraude.Net/ApiDenuncias/Services/GlobalLeaksClient.cs
similarity index 97%
rename from Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksClient.cs
rename to Antifraude.Net/ApiDenuncias/Services/GlobalLeaksClient.cs
index f012f4f..5c608dd 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksClient.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/GlobalLeaksClient.cs
@@ -5,12 +5,12 @@ using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
-using GestionaDenunciasAN.Configuration;
-using GestionaDenunciasAN.Models;
+using ApiDenuncias.Configuration;
+using GestionaDenuncias.Shared.Models;
using Konscious.Security.Cryptography;
using Microsoft.Extensions.Options;
-namespace GestionaDenunciasAN.Services;
+namespace ApiDenuncias.Services;
public sealed class GlobalLeaksClient
{
@@ -23,11 +23,25 @@ public sealed class GlobalLeaksClient
{
_options = options.Value;
_logger = logger;
- _httpClient = new HttpClient
+
+ var handler = new HttpClientHandler();
+ if (_options.AllowInvalidCertificate)
+ {
+ _logger.LogWarning("GlobalLeaks permite certificados TLS no validos. Usar solo temporalmente en PRE.");
+ handler.ServerCertificateCustomValidationCallback =
+ HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
+ }
+
+ _httpClient = new HttpClient(handler)
{
BaseAddress = new Uri(_options.BaseUrl.TrimEnd('/')),
Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds),
};
+
+ if (!string.IsNullOrWhiteSpace(_options.HostHeader))
+ {
+ _httpClient.DefaultRequestHeaders.Host = _options.HostHeader.Trim();
+ }
}
public async Task LoginAsync(
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksExceptions.cs b/Antifraude.Net/ApiDenuncias/Services/GlobalLeaksExceptions.cs
similarity index 86%
rename from Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksExceptions.cs
rename to Antifraude.Net/ApiDenuncias/Services/GlobalLeaksExceptions.cs
index b58bf61..ac412a6 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksExceptions.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/GlobalLeaksExceptions.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Services;
+namespace ApiDenuncias.Services;
public sealed class GlobalLeaksValidationException(string message, int statusCode = 400) : Exception(message)
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksSessionStore.cs b/Antifraude.Net/ApiDenuncias/Services/GlobalLeaksSessionStore.cs
similarity index 98%
rename from Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksSessionStore.cs
rename to Antifraude.Net/ApiDenuncias/Services/GlobalLeaksSessionStore.cs
index df0f816..330a937 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksSessionStore.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/GlobalLeaksSessionStore.cs
@@ -1,10 +1,10 @@
using System.Text;
using System.Text.Json;
using System.Security.Cryptography;
-using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
using Microsoft.AspNetCore.DataProtection;
-namespace GestionaDenunciasAN.Services;
+namespace ApiDenuncias.Services;
public sealed class GlobalLeaksSessionStore
{
diff --git a/Antifraude.Net/ApiDenuncias/Services/IEncryptionKeyProvider.cs b/Antifraude.Net/ApiDenuncias/Services/IEncryptionKeyProvider.cs
new file mode 100644
index 0000000..c99ee49
--- /dev/null
+++ b/Antifraude.Net/ApiDenuncias/Services/IEncryptionKeyProvider.cs
@@ -0,0 +1,6 @@
+namespace ApiDenuncias.Services;
+
+public interface IEncryptionKeyProvider
+{
+ ValueTask GetKeyAsync(CancellationToken cancellationToken = default);
+}
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/IGestionaService.cs b/Antifraude.Net/ApiDenuncias/Services/IGestionaService.cs
similarity index 88%
rename from Antifraude.Net/GestionaDenunciasAN/Services/IGestionaService.cs
rename to Antifraude.Net/ApiDenuncias/Services/IGestionaService.cs
index bebe998..6b0f067 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/IGestionaService.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/IGestionaService.cs
@@ -1,9 +1,9 @@
-using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
-namespace GestionaDenunciasAN.Services
+namespace ApiDenuncias.Services
{
public interface IGestionaService
{
@@ -22,7 +22,7 @@ namespace GestionaDenunciasAN.Services
);
///
- /// Abre el expediente (lo pone en OPEN_EDITABLE), asigna título, clasificación y lo vincula al grupo indicado.
+ /// Abre el expediente (lo pone en OPEN_EDITABLE), asigna ttulo, clasificacin y lo vincula al grupo indicado.
///
Task OpenFileAsync(
string fileUrl,
@@ -47,7 +47,7 @@ namespace GestionaDenunciasAN.Services
);
///
- /// Crea el documento (metadata) y sube el contenido PDF a la raíz o a una carpeta.
+ /// Crea el documento (metadata) y sube el contenido PDF a la raz o a una carpeta.
///
Task UploadDocumentAsync(
string fileUrl,
@@ -56,7 +56,7 @@ namespace GestionaDenunciasAN.Services
);
///
- /// Crea una carpeta de nombre 'folderName' en la raíz del expediente y devuelve su GUID.
+ /// Crea una carpeta de nombre 'folderName' en la raz del expediente y devuelve su GUID.
///
Task CreateFolderAsync(
string fileUrl,
@@ -89,9 +89,9 @@ namespace GestionaDenunciasAN.Services
///
/// Usa el NIF tal cual viene.
- /// Si es anónimo o vacío → no crea ni enlaza.
+ /// Si es annimo o vaco ? no crea ni enlaza.
/// Si no existe, lo crea.
- /// Si no está enlazado al expediente, lo enlaza.
+ /// Si no est enlazado al expediente, lo enlaza.
///
Task AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty);
@@ -102,13 +102,13 @@ namespace GestionaDenunciasAN.Services
// =========================
///
- /// Devuelve el JSON crudo del listado operativo de expedientes, sin recorrer histórico paginado.
+ /// Devuelve el JSON crudo del listado operativo de expedientes, sin recorrer histrico paginado.
///
Task ListarExpedientesJsonAsyncBasico(int maxPages = 1);
///
/// Busca directamente un expediente cuyo asunto sea "Denuncia {idDenuncia}-CD".
- /// Devuelve URL, número de expediente y título si lo encuentra; null si no.
+ /// Devuelve URL, nmero de expediente y ttulo si lo encuentra; null si no.
///
Task BuscarExpedientePorIdEnAsuntoAsync(int idDenuncia);
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/InboxTrackingService.cs b/Antifraude.Net/ApiDenuncias/Services/InboxTrackingService.cs
similarity index 86%
rename from Antifraude.Net/GestionaDenunciasAN/Services/InboxTrackingService.cs
rename to Antifraude.Net/ApiDenuncias/Services/InboxTrackingService.cs
index a93c75e..d5e02b2 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/InboxTrackingService.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/InboxTrackingService.cs
@@ -1,22 +1,20 @@
using System.Globalization;
-using GestionaDenunciasAN.Configuration;
-using GestionaDenunciasAN.Models;
-using Microsoft.Extensions.Options;
+using GestionaDenuncias.Shared.Models;
using MySqlConnector;
-namespace GestionaDenunciasAN.Services;
+namespace ApiDenuncias.Services;
public sealed class InboxTrackingService : IInboxTrackingService
{
- private readonly ComplaintStorageOptions _options;
private readonly IDenunciaStore _denunciaStore;
+ private readonly MySqlConnectionStringProvider _connectionStringProvider;
public InboxTrackingService(
- IOptions options,
- IDenunciaStore denunciaStore)
+ IDenunciaStore denunciaStore,
+ MySqlConnectionStringProvider connectionStringProvider)
{
- _options = options.Value;
_denunciaStore = denunciaStore;
+ _connectionStringProvider = connectionStringProvider;
}
public async Task GetUserStateAsync(string username, CancellationToken cancellationToken = default)
@@ -107,9 +105,49 @@ public sealed class InboxTrackingService : IInboxTrackingService
TrackingNote = BuildTrackingNote(meta)
};
})
+ .Where(report => !IsLockedByAnotherUser(report))
.ToArray();
}
+ public async Task EnsureReportCanBeImportedByUserAsync(
+ string username,
+ ReportDto report,
+ CancellationToken cancellationToken = default)
+ {
+ await _denunciaStore.EnsureSchemaAsync(cancellationToken);
+
+ if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(report.Id))
+ {
+ throw new InvalidOperationException("No se ha podido validar la propiedad de la denuncia.");
+ }
+
+ await using var connection = await OpenConnectionAsync(cancellationToken);
+ var userId = await EnsureUserAsync(connection, username, cancellationToken);
+ await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
+
+ try
+ {
+ await UpsertInboxReportAsync(connection, (MySqlTransaction)transaction, report, cancellationToken);
+ await transaction.CommitAsync(cancellationToken);
+ }
+ catch
+ {
+ await transaction.RollbackAsync(cancellationToken);
+ throw;
+ }
+
+ var metadata = await LoadMetadataAsync(connection, userId, [report.Id], cancellationToken);
+ if (metadata.TryGetValue(report.Id, out var meta) && meta.LockedByAnotherUser)
+ {
+ var owner = string.IsNullOrWhiteSpace(meta.LastDownloadedByUsername)
+ ? "otro usuario"
+ : meta.LastDownloadedByUsername;
+
+ throw new InvalidOperationException(
+ $"La denuncia ya fue importada por {owner}. Solo ese usuario puede ver e importar sus actualizaciones.");
+ }
+ }
+
public async Task MarkReportImportedAsync(
string username,
ReportDto report,
@@ -390,9 +428,14 @@ public sealed class InboxTrackingService : IInboxTrackingService
? null
: reader.GetString(reader.GetOrdinal("last_downloaded_by_username"));
var downloadedByCurrentUser = reader.GetInt32(reader.GetOrdinal("downloaded_by_current_user")) == 1;
- var downloadedByAnotherUser =
- !downloadedByCurrentUser &&
- !string.IsNullOrWhiteSpace(lastDownloadedByUsername);
+ var lockedByAnotherUser =
+ !downloadedByCurrentUser &&
+ !reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")) &&
+ !string.IsNullOrWhiteSpace(lastDownloadedByUsername);
+
+ var downloadedByAnotherUser =
+ !downloadedByCurrentUser &&
+ !string.IsNullOrWhiteSpace(lastDownloadedByUsername);
metadata[reportId] = new ReportMetadata
{
@@ -402,6 +445,7 @@ public sealed class InboxTrackingService : IInboxTrackingService
LastDownloadedAtUtc = GetDateTimeOffset(reader, "last_downloaded_at_utc"),
AlreadyImported = !reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")),
AlreadyInGestiona = reader.GetInt32(reader.GetOrdinal("already_in_gestiona")) == 1,
+ LockedByAnotherUser = lockedByAnotherUser,
};
}
@@ -410,13 +454,8 @@ public sealed class InboxTrackingService : IInboxTrackingService
private async Task OpenConnectionAsync(CancellationToken cancellationToken)
{
- if (string.IsNullOrWhiteSpace(_options.ConnectionString))
- {
- throw new InvalidOperationException(
- "Falta configurar ComplaintStorage:ConnectionString en appsettings.json.");
- }
-
- var connection = new MySqlConnection(_options.ConnectionString);
+ var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
+ var connection = new MySqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
await using var timeZoneCommand = new MySqlCommand("SET time_zone = '+00:00';", connection);
await timeZoneCommand.ExecuteNonQueryAsync(cancellationToken);
@@ -482,6 +521,13 @@ public sealed class InboxTrackingService : IInboxTrackingService
return null;
}
+ if (metadata.LockedByAnotherUser)
+ {
+ return string.IsNullOrWhiteSpace(metadata.LastDownloadedByUsername)
+ ? "Importada por otro usuario"
+ : $"Importada por {metadata.LastDownloadedByUsername}";
+ }
+
if (metadata.AlreadyInGestiona)
{
return "Ya existe expediente en Gestiona";
@@ -514,9 +560,15 @@ public sealed class InboxTrackingService : IInboxTrackingService
{
public bool DownloadedByCurrentUser { get; init; }
public bool DownloadedByAnotherUser { get; init; }
+ public bool LockedByAnotherUser { get; init; }
public string? LastDownloadedByUsername { get; init; }
public DateTimeOffset? LastDownloadedAtUtc { get; init; }
public bool AlreadyImported { get; init; }
public bool AlreadyInGestiona { get; init; }
}
+
+ private static bool IsLockedByAnotherUser(ReportDto report)
+ => report.AlreadyImported &&
+ report.DownloadedByAnotherUser &&
+ !report.DownloadedByCurrentUser;
}
diff --git a/Antifraude.Net/ApiDenuncias/Services/KeyVaultEncryptionKeyProvider.cs b/Antifraude.Net/ApiDenuncias/Services/KeyVaultEncryptionKeyProvider.cs
new file mode 100644
index 0000000..d160817
--- /dev/null
+++ b/Antifraude.Net/ApiDenuncias/Services/KeyVaultEncryptionKeyProvider.cs
@@ -0,0 +1,115 @@
+using System.Security.Cryptography;
+using System.Text;
+using ApiDenuncias.Configuration;
+using Azure;
+using Azure.Core;
+using Azure.Identity;
+using Azure.Security.KeyVault.Secrets;
+using Microsoft.Extensions.Options;
+
+namespace ApiDenuncias.Services;
+
+public sealed class KeyVaultEncryptionKeyProvider : IEncryptionKeyProvider
+{
+ private readonly KeyVaultOptions _options;
+ private readonly IConfiguration _configuration;
+ private readonly ILogger _logger;
+ private readonly Lazy> _keyLoader;
+
+ public KeyVaultEncryptionKeyProvider(
+ IOptions options,
+ IConfiguration configuration,
+ ILogger logger)
+ {
+ _options = options.Value;
+ _configuration = configuration;
+ _logger = logger;
+ _keyLoader = new Lazy>(LoadKeyAsync);
+ }
+
+ public async ValueTask GetKeyAsync(CancellationToken cancellationToken = default)
+ {
+ var key = await _keyLoader.Value.WaitAsync(cancellationToken);
+ return key.ToArray();
+ }
+
+ private async Task LoadKeyAsync()
+ {
+ var configuredLocalKey = _configuration["Encryption:LocalDevelopmentKey"];
+ if (!_options.Enabled)
+ {
+ if (string.IsNullOrWhiteSpace(configuredLocalKey))
+ {
+ throw new InvalidOperationException(
+ "Key Vault esta deshabilitado y no se ha configurado Encryption:LocalDevelopmentKey.");
+ }
+
+ _logger.LogWarning("Key Vault deshabilitado. Usando clave local solo para pruebas de desarrollo.");
+ return NormalizeKey(configuredLocalKey);
+ }
+
+ if (string.IsNullOrWhiteSpace(_options.VaultUrl))
+ {
+ throw new InvalidOperationException("KeyVault:VaultUrl no esta configurado.");
+ }
+
+ if (string.IsNullOrWhiteSpace(_options.EncryptionKeySecretName))
+ {
+ throw new InvalidOperationException("KeyVault:EncryptionKeySecretName no esta configurado.");
+ }
+ var credential = new DefaultAzureCredential();
+ var client = new SecretClient(new Uri(_options.VaultUrl), credential);
+ KeyVaultSecret secret;
+ try
+ {
+ var response = await client.GetSecretAsync(_options.EncryptionKeySecretName);
+ secret = response.Value;
+ }
+ catch (RequestFailedException ex) when (ex.Status == StatusCodes.Status404NotFound && _options.AllowLocalEncryptionKeyFallback)
+ {
+ if (string.IsNullOrWhiteSpace(configuredLocalKey))
+ {
+ throw new InvalidOperationException(
+ $"El secreto '{_options.EncryptionKeySecretName}' no existe en Key Vault y no se ha configurado Encryption:LocalDevelopmentKey.");
+ }
+
+ _logger.LogWarning(
+ "El secreto {SecretName} no existe en Key Vault. Usando clave local temporal por AllowLocalEncryptionKeyFallback=true. No usar en produccion real.",
+ _options.EncryptionKeySecretName);
+ return NormalizeKey(configuredLocalKey);
+ }
+
+ if (string.IsNullOrWhiteSpace(secret.Value))
+ {
+ throw new InvalidOperationException(
+ $"El secreto '{_options.EncryptionKeySecretName}' de Key Vault esta vacio.");
+ }
+
+ _logger.LogInformation(
+ "Clave de cifrado cargada desde Key Vault {VaultUrl} usando el secreto {SecretName}.",
+ _options.VaultUrl,
+ _options.EncryptionKeySecretName);
+
+ return NormalizeKey(secret.Value);
+ }
+
+ private static byte[] NormalizeKey(string secretValue)
+ {
+ var trimmed = secretValue.Trim();
+
+ try
+ {
+ var base64Key = Convert.FromBase64String(trimmed);
+ if (base64Key.Length is 16 or 24 or 32)
+ {
+ return base64Key;
+ }
+ }
+ catch (FormatException)
+ {
+ // Si no es base64, derivamos una clave estable desde el valor textual.
+ }
+
+ return SHA256.HashData(Encoding.UTF8.GetBytes(trimmed));
+ }
+}
diff --git a/Antifraude.Net/ApiDenuncias/Services/MySqlConnectionStringProvider.cs b/Antifraude.Net/ApiDenuncias/Services/MySqlConnectionStringProvider.cs
new file mode 100644
index 0000000..1846d1d
--- /dev/null
+++ b/Antifraude.Net/ApiDenuncias/Services/MySqlConnectionStringProvider.cs
@@ -0,0 +1,124 @@
+using ApiDenuncias.Configuration;
+using Azure;
+using Azure.Identity;
+using Azure.Security.KeyVault.Secrets;
+using Microsoft.Extensions.Options;
+using MySqlConnector;
+
+namespace ApiDenuncias.Services;
+
+public sealed class MySqlConnectionStringProvider
+{
+ private readonly ComplaintStorageOptions _storageOptions;
+ private readonly KeyVaultOptions _keyVaultOptions;
+ private readonly ILogger _logger;
+
+ public MySqlConnectionStringProvider(
+ IOptions storageOptions,
+ IOptions keyVaultOptions,
+ ILogger logger)
+ {
+ _storageOptions = storageOptions.Value;
+ _keyVaultOptions = keyVaultOptions.Value;
+ _logger = logger;
+ }
+
+ public async ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default)
+ {
+ return await LoadConnectionStringAsync().WaitAsync(cancellationToken);
+ }
+
+ private async Task LoadConnectionStringAsync()
+ {
+ if (!_storageOptions.UseKeyVault || !_keyVaultOptions.Enabled)
+ {
+ if (string.IsNullOrWhiteSpace(_storageOptions.ConnectionString))
+ {
+ throw new InvalidOperationException(
+ "Falta configurar ComplaintStorage:ConnectionString o activar Key Vault para obtener la conexion MySQL.");
+ }
+
+ _logger.LogWarning("Conexion MySQL cargada desde appsettings. Usar solo para desarrollo/local.");
+ return _storageOptions.ConnectionString;
+ }
+
+ if (string.IsNullOrWhiteSpace(_keyVaultOptions.VaultUrl))
+ {
+ throw new InvalidOperationException("KeyVault:VaultUrl no esta configurado.");
+ }
+
+ var client = new SecretClient(new Uri(_keyVaultOptions.VaultUrl), new DefaultAzureCredential());
+ var host = await GetRequiredSecretAsync(client, _storageOptions.HostSecretName);
+ var user = await GetRequiredSecretAsync(client, _storageOptions.UserSecretName);
+ var password = await GetRequiredSecretAsync(client, _storageOptions.PasswordSecretName);
+ var database = await GetRequiredSecretAsync(client, _storageOptions.DatabaseSecretName);
+ var port = await GetOptionalUIntSecretAsync(client, _storageOptions.PortSecretName, _storageOptions.DefaultPort);
+ var sslMode = await GetOptionalSecretAsync(client, _storageOptions.SslModeSecretName, _storageOptions.DefaultSslMode);
+
+ var builder = new MySqlConnectionStringBuilder
+ {
+ Server = host,
+ Port = port,
+ UserID = user,
+ Password = password,
+ Database = database,
+ SslMode = ParseSslMode(sslMode),
+ };
+
+ _logger.LogInformation(
+ "Conexion MySQL cargada desde Key Vault {VaultUrl}. Host={Host}; Database={Database}; User={User}; Port={Port}.",
+ _keyVaultOptions.VaultUrl,
+ host,
+ database,
+ user,
+ port);
+
+ return builder.ConnectionString;
+ }
+
+ private static async Task GetRequiredSecretAsync(SecretClient client, string secretName)
+ {
+ var value = await GetOptionalSecretAsync(client, secretName, null);
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new InvalidOperationException($"El secreto obligatorio '{secretName}' de Key Vault no existe o esta vacio.");
+ }
+
+ return value.Trim();
+ }
+
+ private static async Task GetOptionalSecretAsync(SecretClient client, string secretName, string? fallback)
+ {
+ if (string.IsNullOrWhiteSpace(secretName))
+ {
+ return fallback ?? string.Empty;
+ }
+
+ try
+ {
+ var secret = await client.GetSecretAsync(secretName.Trim());
+ return string.IsNullOrWhiteSpace(secret.Value.Value)
+ ? fallback ?? string.Empty
+ : secret.Value.Value.Trim();
+ }
+ catch (RequestFailedException ex) when (ex.Status == StatusCodes.Status404NotFound)
+ {
+ return fallback ?? string.Empty;
+ }
+ }
+
+ private static async Task GetOptionalUIntSecretAsync(SecretClient client, string secretName, uint fallback)
+ {
+ var value = await GetOptionalSecretAsync(client, secretName, null);
+ return uint.TryParse(value, out var parsed) && parsed > 0
+ ? parsed
+ : fallback;
+ }
+
+ private static MySqlSslMode ParseSslMode(string? value)
+ {
+ return Enum.TryParse(value, ignoreCase: true, out var parsed)
+ ? parsed
+ : MySqlSslMode.Required;
+ }
+}
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/MySqlDenunciaStore.cs b/Antifraude.Net/ApiDenuncias/Services/MySqlDenunciaStore.cs
similarity index 98%
rename from Antifraude.Net/GestionaDenunciasAN/Services/MySqlDenunciaStore.cs
rename to Antifraude.Net/ApiDenuncias/Services/MySqlDenunciaStore.cs
index 5cba7eb..4768b0c 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/MySqlDenunciaStore.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/MySqlDenunciaStore.cs
@@ -1,12 +1,12 @@
using System.Data;
using System.Globalization;
using System.Security.Cryptography;
-using GestionaDenunciasAN.Configuration;
-using GestionaDenunciasAN.Models;
+using ApiDenuncias.Configuration;
+using GestionaDenuncias.Shared.Models;
using Microsoft.Extensions.Options;
using MySqlConnector;
-namespace GestionaDenunciasAN.Services;
+namespace ApiDenuncias.Services;
public sealed class MySqlDenunciaStore : IDenunciaStore
{
@@ -103,15 +103,18 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
private readonly ILogger _logger;
private static readonly SemaphoreSlim SchemaGate = new(1, 1);
private static volatile bool SchemaEnsured;
+ private readonly MySqlConnectionStringProvider _connectionStringProvider;
public MySqlDenunciaStore(
IOptions options,
IHostEnvironment environment,
- ILogger logger)
+ ILogger logger,
+ MySqlConnectionStringProvider connectionStringProvider)
{
_options = options.Value;
_environment = environment;
_logger = logger;
+ _connectionStringProvider = connectionStringProvider;
}
public Task EnsureSchemaAsync(CancellationToken cancellationToken = default)
@@ -705,12 +708,12 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
content_mime_type = @contentMimeType,
content_sha256 = @contentSha256,
uploaded_to_gestiona = CASE
- WHEN LOWER(@originalFileName) = 'report.txt' THEN @uploadedToGestiona
+ WHEN LOWER(@originalFileName) = 'report.txt' OR LOWER(@originalFileName) = 'report.pdf' THEN @uploadedToGestiona
WHEN complaint_attachments.content_sha256 = @contentSha256 THEN complaint_attachments.uploaded_to_gestiona
ELSE @uploadedToGestiona
END,
uploaded_at_utc = CASE
- WHEN LOWER(@originalFileName) = 'report.txt' THEN @uploadedAtUtc
+ WHEN LOWER(@originalFileName) = 'report.txt' OR LOWER(@originalFileName) = 'report.pdf' THEN @uploadedAtUtc
WHEN complaint_attachments.content_sha256 = @contentSha256 THEN complaint_attachments.uploaded_at_utc
ELSE @uploadedAtUtc
END,
@@ -850,13 +853,8 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
private async Task OpenConnectionAsync(CancellationToken cancellationToken)
{
- if (string.IsNullOrWhiteSpace(_options.ConnectionString))
- {
- throw new InvalidOperationException(
- "Falta configurar ComplaintStorage:ConnectionString en appsettings.json.");
- }
-
- var connection = new MySqlConnection(_options.ConnectionString);
+ var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
+ var connection = new MySqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
await using var timeZoneCommand = new MySqlCommand("SET time_zone = '+00:00';", connection);
diff --git a/Antifraude.Net/ApiDenuncias/Services/UserComplaintAccessService.cs b/Antifraude.Net/ApiDenuncias/Services/UserComplaintAccessService.cs
index 99be9e4..0adde92 100644
--- a/Antifraude.Net/ApiDenuncias/Services/UserComplaintAccessService.cs
+++ b/Antifraude.Net/ApiDenuncias/Services/UserComplaintAccessService.cs
@@ -1,22 +1,20 @@
using System.Globalization;
-using GestionaDenunciasAN.Configuration;
-using Microsoft.Extensions.Options;
using MySqlConnector;
namespace ApiDenuncias.Services;
public sealed class UserComplaintAccessService
{
- private readonly ComplaintStorageOptions _options;
+ private readonly MySqlConnectionStringProvider _connectionStringProvider;
- public UserComplaintAccessService(IOptions options)
+ public UserComplaintAccessService(MySqlConnectionStringProvider connectionStringProvider)
{
- _options = options.Value;
+ _connectionStringProvider = connectionStringProvider;
}
public async Task> GetAllowedComplaintIdsAsync(string username, CancellationToken cancellationToken = default)
{
- if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(_options.ConnectionString))
+ if (string.IsNullOrWhiteSpace(username))
{
return [];
}
@@ -31,7 +29,8 @@ public sealed class UserComplaintAccessService
AND uir.download_count > 0;
""";
- await using var connection = new MySqlConnection(_options.ConnectionString);
+ var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
+ await using var connection = new MySqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
await using var command = new MySqlCommand(sql, connection);
diff --git a/Antifraude.Net/ApiDenuncias/appsettings.Development.json b/Antifraude.Net/ApiDenuncias/appsettings.Development.json
index 0c208ae..7e84ef1 100644
--- a/Antifraude.Net/ApiDenuncias/appsettings.Development.json
+++ b/Antifraude.Net/ApiDenuncias/appsettings.Development.json
@@ -4,5 +4,16 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
+ },
+ "KeyVault": {
+ "Enabled": false
+ },
+ "Encryption": {
+ "LocalDevelopmentKey": "local-development-only-denuncias-encryption-key"
+ },
+ "ComplaintStorage": {
+ "ConnectionString": "Server=192.168.41.25;Port=13306;Database=gestiondenuncias;Uid=tecnosis;Pwd=tsl4net.Ts87;",
+ "UseKeyVault": false,
+ "AutoCreateSchema": true
}
}
diff --git a/Antifraude.Net/ApiDenuncias/appsettings.json b/Antifraude.Net/ApiDenuncias/appsettings.json
index e924468..6d06087 100644
--- a/Antifraude.Net/ApiDenuncias/appsettings.json
+++ b/Antifraude.Net/ApiDenuncias/appsettings.json
@@ -6,15 +6,32 @@
}
},
"AllowedHosts": "*",
+ "DetailedApiErrors": true,
"Jwt": {
"Issuer": "ApiDenuncias",
"Audience": "GestionaDenunciasAN",
"SigningKey": "dev-local-api-denuncias-jwt-signing-key-please-change",
- "ExpirationMinutes": 480
+ "ExpirationMinutes": 480,
+ "RequireHttpsMetadata": false
+ },
+ "ForceHttpsRedirection": false,
+ "KeyVault": {
+ "Enabled": true,
+ "VaultUrl": "https://oaaf-kv-pre.vault.azure.net",
+ "EncryptionKeySecretName": "denuncias-encryption-key",
+ "AllowLocalEncryptionKeyFallback": true
+ },
+ "Encryption": {
+ "LocalDevelopmentKey": "presentacion-pre-denuncias-encryption-key-cambiar-antes-de-produccion"
},
"Gestiona": {
"ApiBase": "https://02.g3stiona.com",
"AccessToken": "_yr.xVvPOllsyd1TYZRxUxg__c",
+ "CircuitTemplateId": "bb997758-7436-46ab-9dc3-50dce2e02cfa",
+ "CircuitSignerStampHref": "https://02.g3stiona.com/rest/organ-stamps/3c6eaab4-7fcd-4b21-8676-bf8719be5d36",
+ "CircuitSignerStampTitle": "oaaf-complaints-tramit",
+ "CircuitRecipientGroupHref": "https://02.g3stiona.com/rest/groups/454fa4ec-8b82-4240-9419-113f45d4b004",
+ "CircuitVersion": "2",
"PreferredCircuitTemplateName": "CT-Actualización de denuncia",
"UserLink": "https://02.g3stiona.com/rest/users/0c168833-8e27-4695-a301-b79924031f63",
"GroupLink": "https://02.g3stiona.com/rest/groups/6dbfc433-1eb6-4b9a-a533-bfebc652c101",
@@ -22,11 +39,22 @@
},
"GlobalLeaks": {
"BaseUrl": "https://prebuzon.antifraudeandalucia.es",
+ "HostHeader": "",
+ "AllowInvalidCertificate": true,
"TimeoutSeconds": 120,
"MaxDownloadBytes": 524288000
},
"ComplaintStorage": {
- "ConnectionString": "Server=192.168.41.25;Port=13306;Database=gestiondenuncias;Uid=tecnosis;Pwd=tsl4net.Ts87;",
+ "ConnectionString": "",
+ "UseKeyVault": true,
+ "HostSecretName": "bbdd-host",
+ "UserSecretName": "bbdd-user",
+ "PasswordSecretName": "bbdd-password",
+ "DatabaseSecretName": "bbdd-name",
+ "PortSecretName": "bbdd-port",
+ "SslModeSecretName": "bbdd-ssl-mode",
+ "DefaultPort": 3306,
+ "DefaultSslMode": "Required",
"AutoCreateSchema": true
}
}
diff --git a/Antifraude.Net/GestionaDenuncias.Shared/GestionaDenuncias.Shared.csproj b/Antifraude.Net/GestionaDenuncias.Shared/GestionaDenuncias.Shared.csproj
new file mode 100644
index 0000000..f4987a0
--- /dev/null
+++ b/Antifraude.Net/GestionaDenuncias.Shared/GestionaDenuncias.Shared.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Antifraude.Net/GestionaDenunciasAN/Helpers/PdfHelper.cs b/Antifraude.Net/GestionaDenuncias.Shared/Helpers/PdfHelper.cs
similarity index 91%
rename from Antifraude.Net/GestionaDenunciasAN/Helpers/PdfHelper.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Helpers/PdfHelper.cs
index 0ce6997..a1bd4c4 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Helpers/PdfHelper.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Helpers/PdfHelper.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
@@ -6,14 +6,14 @@ using PdfSharpCore.Drawing;
using PdfSharpCore.Pdf;
using PdfSharpCore.Pdf.IO;
-namespace GestionaDenunciasAN.Helpers
+namespace GestionaDenuncias.Shared.Helpers
{
public static class PdfHelper
{
///
- /// Fusiona varios ficheros (PDF, imágenes, TXT) en un único PDF.
- /// Los .txt se renderizan con márgenes iguales, alineación a la izquierda y ajuste de líneas,
- /// preservando líneas en blanco.
+ /// Fusiona varios ficheros (PDF, imgenes, TXT) en un nico PDF.
+ /// Los .txt se renderizan con mrgenes iguales, alineacin a la izquierda y ajuste de lneas,
+ /// preservando lneas en blanco.
///
/// Secuencia de tuplas (FileName, ContentBytes)
/// Bytes del PDF combinado
@@ -52,7 +52,7 @@ namespace GestionaDenunciasAN.Helpers
break;
case ".txt":
- // Renderizado de TXT con margen y ajuste de líneas, preservando líneas en blanco
+ // Renderizado de TXT con margen y ajuste de lneas, preservando lneas en blanco
var text = Encoding.UTF8.GetString(content);
PdfPage pageTxt = outputDoc.AddPage();
XGraphics gfxTxt = XGraphics.FromPdfPage(pageTxt);
@@ -72,7 +72,7 @@ namespace GestionaDenunciasAN.Helpers
foreach (var origLine in text.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n'))
{
- // Línea en blanco: preservarla
+ // Lnea en blanco: preservarla
if (string.IsNullOrWhiteSpace(origLine))
{
y += lineHeight;
@@ -99,7 +99,7 @@ namespace GestionaDenunciasAN.Helpers
}
else
{
- // Dibujar la línea acumulada
+ // Dibujar la lnea acumulada
gfxTxt.DrawString(
currentLine,
font,
@@ -109,7 +109,7 @@ namespace GestionaDenunciasAN.Helpers
y += lineHeight;
currentLine = word;
- // Paginación si se sale por abajo
+ // Paginacin si se sale por abajo
if (y + lineHeight > pageHeight - marginBottom)
{
gfxTxt.Dispose();
@@ -120,7 +120,7 @@ namespace GestionaDenunciasAN.Helpers
}
}
- // Dibujar la última línea del párrafo
+ // Dibujar la ltima lnea del prrafo
if (!string.IsNullOrEmpty(currentLine))
{
gfxTxt.DrawString(
@@ -145,7 +145,7 @@ namespace GestionaDenunciasAN.Helpers
break;
default:
- throw new NotSupportedException($"Extensión no soportada: {ext}");
+ throw new NotSupportedException($"Extensin no soportada: {ext}");
}
}
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/ApiDenunciasDtos.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ApiDenunciasDtos.cs
similarity index 93%
rename from Antifraude.Net/GestionaDenunciasAN/Models/ApiDenunciasDtos.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/ApiDenunciasDtos.cs
index 4d0cf0a..15f455b 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/ApiDenunciasDtos.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ApiDenunciasDtos.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed record ApiLoginResponse(
string Username,
@@ -36,6 +36,10 @@ public sealed record MarkReportImportedRequest(
ReportDto Report,
int? ComplaintId);
+public sealed record TrackingImportPermissionRequest(
+ string Username,
+ ReportDto Report);
+
public sealed record GestionaCreateFileRequest(
Guid ProcedureId,
string Subject,
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/ApiError.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ApiError.cs
similarity index 63%
rename from Antifraude.Net/GestionaDenunciasAN/Models/ApiError.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/ApiError.cs
index a3f60d4..6993923 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/ApiError.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ApiError.cs
@@ -1,3 +1,3 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed record ApiError(string Error, bool SessionExpired = false);
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/ContextDto.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ContextDto.cs
similarity index 57%
rename from Antifraude.Net/GestionaDenunciasAN/Models/ContextDto.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/ContextDto.cs
index 4d3cc2f..6301925 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/ContextDto.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ContextDto.cs
@@ -1,3 +1,3 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed record ContextDto(string Id, string Name);
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/DenunciasGestiona.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/DenunciasGestiona.cs
similarity index 99%
rename from Antifraude.Net/GestionaDenunciasAN/Models/DenunciasGestiona.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/DenunciasGestiona.cs
index 46ca107..774f567 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/DenunciasGestiona.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/DenunciasGestiona.cs
@@ -3,7 +3,7 @@ using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public class DenunciasGestiona
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/ExpedienteTerceroDto.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ExpedienteTerceroDto.cs
similarity index 89%
rename from Antifraude.Net/GestionaDenunciasAN/Models/ExpedienteTerceroDto.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/ExpedienteTerceroDto.cs
index 72f77b7..468cc75 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/ExpedienteTerceroDto.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ExpedienteTerceroDto.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models
+namespace GestionaDenuncias.Shared.Models
{
public class ExpedienteTerceroDto
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/FicherosDenuncias.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/FicherosDenuncias.cs
similarity index 72%
rename from Antifraude.Net/GestionaDenunciasAN/Models/FicherosDenuncias.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/FicherosDenuncias.cs
index fb72058..9a7515c 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/FicherosDenuncias.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/FicherosDenuncias.cs
@@ -1,7 +1,7 @@
// Models/FicherosDenuncias.cs
using System;
-namespace GestionaDenunciasAN.Models
+namespace GestionaDenuncias.Shared.Models
{
public class FicherosDenuncias
{
@@ -29,17 +29,26 @@ namespace GestionaDenunciasAN.Models
// Fichero completo en formato byte array (BLOB)
public byte[] Fichero { get; set; } = [];
- // → Nuevo: marca si ya se subió a Gestión
+ // ? Nuevo: marca si ya se subió a Gestión
public bool Subido { get; set; }
- // → Nuevo: fecha en que se subió por última vez
+ // ? Nuevo: fecha en que se subió por última vez
public DateTime? FechaSubida { get; set; }
// Hash SHA-256 del contenido, para evitar re-subir adjuntos repetidos.
public string ContentSha256 { get; set; } = string.Empty;
- public bool EsReport =>
- string.Equals(NombreFichero, "report.txt", StringComparison.OrdinalIgnoreCase);
+ public bool EsReport
+ {
+ get
+ {
+ var fileName = System.IO.Path.GetFileNameWithoutExtension(NombreFichero);
+ var extension = System.IO.Path.GetExtension(NombreFichero);
+ return fileName.StartsWith("report", StringComparison.OrdinalIgnoreCase) &&
+ (extension.Equals(".txt", StringComparison.OrdinalIgnoreCase) ||
+ extension.Equals(".pdf", StringComparison.OrdinalIgnoreCase));
+ }
+ }
public FicherosDenuncias() { }
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/FileDownloadResult.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/FileDownloadResult.cs
similarity index 63%
rename from Antifraude.Net/GestionaDenunciasAN/Models/FileDownloadResult.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/FileDownloadResult.cs
index 3e421cd..92021b7 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/FileDownloadResult.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/FileDownloadResult.cs
@@ -1,3 +1,3 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed record FileDownloadResult(byte[] Content, string FileName);
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/GestionaExpedienteInfo.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/GestionaExpedienteInfo.cs
similarity index 82%
rename from Antifraude.Net/GestionaDenunciasAN/Models/GestionaExpedienteInfo.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/GestionaExpedienteInfo.cs
index 0eb9422..ad071e6 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/GestionaExpedienteInfo.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/GestionaExpedienteInfo.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed class GestionaExpedienteInfo
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/GlSession.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/GlSession.cs
similarity index 65%
rename from Antifraude.Net/GestionaDenunciasAN/Models/GlSession.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/GlSession.cs
index 2248bf5..f5e1c25 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/GlSession.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/GlSession.cs
@@ -1,3 +1,3 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed record GlSession(string Id, string Username, string? Role = null);
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/GlobalLeaksStoredSession.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/GlobalLeaksStoredSession.cs
similarity index 90%
rename from Antifraude.Net/GestionaDenunciasAN/Models/GlobalLeaksStoredSession.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/GlobalLeaksStoredSession.cs
index ce8a7d7..f37c680 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/GlobalLeaksStoredSession.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/GlobalLeaksStoredSession.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed class GlobalLeaksStoredSession
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/ImportSummary.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ImportSummary.cs
similarity index 80%
rename from Antifraude.Net/GestionaDenunciasAN/Models/ImportSummary.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/ImportSummary.cs
index 05b633e..c5c9405 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/ImportSummary.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ImportSummary.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed record ImportSummary(
int TotalCandidates,
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/InboxUserState.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/InboxUserState.cs
similarity index 88%
rename from Antifraude.Net/GestionaDenunciasAN/Models/InboxUserState.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/InboxUserState.cs
index 9a56912..e1c9450 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/InboxUserState.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/InboxUserState.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed record InboxUserState
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/LoginRequest.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/LoginRequest.cs
similarity index 66%
rename from Antifraude.Net/GestionaDenunciasAN/Models/LoginRequest.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/LoginRequest.cs
index e687742..0830bcd 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/LoginRequest.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/LoginRequest.cs
@@ -1,3 +1,3 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed record LoginRequest(string Username, string Password, string Authcode);
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/LoginResponse.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/LoginResponse.cs
similarity index 55%
rename from Antifraude.Net/GestionaDenunciasAN/Models/LoginResponse.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/LoginResponse.cs
index 8544121..a384ea5 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/LoginResponse.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/LoginResponse.cs
@@ -1,3 +1,3 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed record LoginResponse(string Username);
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/ReportDto.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ReportDto.cs
similarity index 95%
rename from Antifraude.Net/GestionaDenunciasAN/Models/ReportDto.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/ReportDto.cs
index b99b7ce..3aa320f 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/ReportDto.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ReportDto.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed record ReportDto
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/ReportFieldEntry.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ReportFieldEntry.cs
similarity index 84%
rename from Antifraude.Net/GestionaDenunciasAN/Models/ReportFieldEntry.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/ReportFieldEntry.cs
index eee247c..e2c3cb6 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/ReportFieldEntry.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ReportFieldEntry.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed class ReportFieldEntry
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/ThirdPartyAddressData.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ThirdPartyAddressData.cs
similarity index 97%
rename from Antifraude.Net/GestionaDenunciasAN/Models/ThirdPartyAddressData.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/ThirdPartyAddressData.cs
index f3ea57b..dce41ff 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/ThirdPartyAddressData.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ThirdPartyAddressData.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed class ThirdPartyAddressData
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/ThirdPartyIdentityData.cs b/Antifraude.Net/GestionaDenuncias.Shared/Models/ThirdPartyIdentityData.cs
similarity index 98%
rename from Antifraude.Net/GestionaDenunciasAN/Models/ThirdPartyIdentityData.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Models/ThirdPartyIdentityData.cs
index 2b68e5b..acef45c 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/ThirdPartyIdentityData.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Models/ThirdPartyIdentityData.cs
@@ -1,6 +1,6 @@
using System;
-namespace GestionaDenunciasAN.Models;
+namespace GestionaDenuncias.Shared.Models;
public sealed class ThirdPartyIdentityData
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/IDenunciaStore.cs b/Antifraude.Net/GestionaDenuncias.Shared/Services/IDenunciaStore.cs
similarity index 92%
rename from Antifraude.Net/GestionaDenunciasAN/Services/IDenunciaStore.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Services/IDenunciaStore.cs
index cfe25a1..a11917c 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/IDenunciaStore.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Services/IDenunciaStore.cs
@@ -1,6 +1,6 @@
-using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
-namespace GestionaDenunciasAN.Services;
+namespace GestionaDenuncias.Shared.Services;
public interface IDenunciaStore
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/IInboxTrackingService.cs b/Antifraude.Net/GestionaDenuncias.Shared/Services/IInboxTrackingService.cs
similarity index 67%
rename from Antifraude.Net/GestionaDenunciasAN/Services/IInboxTrackingService.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Services/IInboxTrackingService.cs
index 8b83a7a..c35ad8d 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/IInboxTrackingService.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Services/IInboxTrackingService.cs
@@ -1,6 +1,6 @@
-using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
-namespace GestionaDenunciasAN.Services;
+namespace GestionaDenuncias.Shared.Services;
public interface IInboxTrackingService
{
@@ -14,4 +14,9 @@ public interface IInboxTrackingService
ReportDto report,
int? complaintId,
CancellationToken cancellationToken = default);
+
+ Task EnsureReportCanBeImportedByUserAsync(
+ string username,
+ ReportDto report,
+ CancellationToken cancellationToken = default);
}
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/LoginRateLimiter.cs b/Antifraude.Net/GestionaDenuncias.Shared/Services/LoginRateLimiter.cs
similarity index 94%
rename from Antifraude.Net/GestionaDenunciasAN/Services/LoginRateLimiter.cs
rename to Antifraude.Net/GestionaDenuncias.Shared/Services/LoginRateLimiter.cs
index dcf2e17..ef85241 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/LoginRateLimiter.cs
+++ b/Antifraude.Net/GestionaDenuncias.Shared/Services/LoginRateLimiter.cs
@@ -1,6 +1,6 @@
using System.Collections.Concurrent;
-namespace GestionaDenunciasAN.Services;
+namespace GestionaDenuncias.Shared.Services;
public sealed class LoginRateLimiter
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/App.razor b/Antifraude.Net/GestionaDenunciasAN/Components/App.razor
index 4e50bac..3d60f55 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/App.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/App.razor
@@ -1,4 +1,4 @@
-
+
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Layout/EmptyLayout.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Layout/EmptyLayout.razor
index fad1723..a852abe 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/Layout/EmptyLayout.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/Layout/EmptyLayout.razor
@@ -1,4 +1,4 @@
-@inherits LayoutComponentBase
+@inherits LayoutComponentBase
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Actualizaciones.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Actualizaciones.razor
index 241710a..f89210d 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Actualizaciones.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Actualizaciones.razor
@@ -1,13 +1,13 @@
@page "/Actualizaciones"
@rendermode InteractiveServer
@attribute [Authorize]
-@using GestionaDenunciasAN.Models
+@using GestionaDenuncias.Shared.Models
@using System.Globalization
@using System.IO
@using System.Linq
@using System.Text
-@using GestionaDenunciasAN.Helpers
+@using GestionaDenuncias.Shared.Helpers
@using GestionaDenunciasAN.Services
@attribute [StreamRendering]
@inject GestionaDenunciasAN.Models.UserState userState
@@ -731,6 +731,19 @@ else
useAutoFoundExpediente = true;
}
+ private static bool IsReportFileName(string? fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ return false;
+ }
+
+ var name = Path.GetFileNameWithoutExtension(fileName);
+ var extension = Path.GetExtension(fileName);
+ return name.StartsWith("report", StringComparison.OrdinalIgnoreCase) &&
+ (extension.Equals(".txt", StringComparison.OrdinalIgnoreCase) ||
+ extension.Equals(".pdf", StringComparison.OrdinalIgnoreCase));
+ }
private string FixFileName(string input)
{
var n = input.Normalize(NormalizationForm.FormD);
@@ -871,7 +884,7 @@ else
string? documentoParaTramitar = null;
var report = todos.FirstOrDefault(t =>
- string.Equals(t.FileName, reportTxt, StringComparison.OrdinalIgnoreCase));
+ IsReportFileName(t.FileName));
if (!string.IsNullOrWhiteSpace(report.FileName))
{
@@ -887,7 +900,7 @@ else
}
var adjuntos = todos
- .Where(t => !string.Equals(t.FileName, reportTxt, StringComparison.OrdinalIgnoreCase))
+ .Where(t => !IsReportFileName(t.FileName))
.ToList();
if (adjuntos.Count > 0 && uploadMode == "merge")
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Buscador.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Buscador.razor
index 9b0e9f2..7edefa3 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Buscador.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Buscador.razor
@@ -1,4 +1,4 @@
-@page "/Buscador"
+@page "/Buscador"
@rendermode InteractiveServer
@attribute [Authorize]
@@ -62,7 +62,7 @@
checked="@IsModo(ModoUltimos)"
@onchange="@(() => SetModo(ModoUltimos))" />
- Últimos X meses
+ ltimos X meses
@@ -90,7 +90,7 @@
{
- Últimos meses
+ ltimos meses
3 meses
6 meses
@@ -100,7 +100,7 @@
}
-
+
- Buscando…
+ Buscando
}
else
{
@@ -145,7 +145,7 @@
Expediente
Asunto
- Fecha creación
+ Fecha creacin
Estado
@@ -177,7 +177,7 @@
Asunto
@exp.Asunto
- Fecha creación
+ Fecha creacin
@exp.FechaCreacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
@@ -228,7 +228,7 @@
private List expedientes = new();
- // expediente cuyo detalle está abierto
+ // expediente cuyo detalle est abierto
private ExpedienteTerceroDto? expedienteSeleccionado;
private bool IsModo(string valor) => string.Equals(modoFecha, valor, StringComparison.Ordinal);
@@ -243,7 +243,7 @@
}
// ============================================================
- // BÚSQUEDA PRINCIPAL
+ // BSQUEDA PRINCIPAL
// ============================================================
private async Task BuscarAsync()
@@ -263,7 +263,7 @@
DateTimeOffset? desde = null;
DateTimeOffset? hasta = null;
- // Calcular rango según el modo
+ // Calcular rango segn el modo
if (modoFecha == ModoRango)
{
if (!fechaDesde.HasValue || !fechaHasta.HasValue)
@@ -285,7 +285,7 @@
{
if (mesesUltimos <= 0)
{
- errorMessage = "El número de meses debe ser mayor que 0.";
+ errorMessage = "El nmero de meses debe ser mayor que 0.";
return;
}
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Error.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Error.razor
index 576cc2d..7474392 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Error.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Error.razor
@@ -1,4 +1,4 @@
-@page "/Error"
+@page "/Error"
@using System.Diagnostics
Error
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/GestionZip.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/GestionZip.razor
index e97f850..13af21b 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/GestionZip.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/GestionZip.razor
@@ -17,9 +17,6 @@
para seguir trayendo denuncias.
-
- @(LocalBusy ? "Procesando..." : "Procesar carpeta local")
-
@if (!string.IsNullOrWhiteSpace(StatusMessage))
@@ -230,53 +227,6 @@
-
-
-
-
-
-
Carpeta local
-
- ZIPs pendientes detectados en C:\ZipsDenuncias.
-
-
-
- @(LocalBusy ? "Procesando..." : "Procesar ZIPs")
-
-
-
- @if (!ExistingZips.Any())
- {
-
No hay ZIPs pendientes en la carpeta.
- }
- else
- {
-
-
-
-
- ZIP
- Acciones
-
-
-
- @foreach (var zip in ExistingZips)
- {
-
- @zip
-
- DeleteZipAsync(zip)">
- Eliminar
-
-
-
- }
-
-
-
- }
-
-
@code {
@@ -285,7 +235,6 @@
private List VisibleReports = [];
private HashSet SelectedIds = [];
- private IReadOnlyList ExistingZips = Array.Empty();
private ApiGlobalLeaksSessionDto? SessionInfo;
private InboxUserState UserInboxState = new();
private string CurrentUsername { get; set; } = string.Empty;
@@ -303,7 +252,6 @@
private bool ReportsBusy { get; set; }
private bool RenewBusy { get; set; }
private bool ImportBusy { get; set; }
- private bool LocalBusy { get; set; }
private bool CanUseGlobalLeaks => SessionInfo?.HasActiveSession == true;
private int SelectedReportsCount => SelectedIds.Count;
@@ -337,8 +285,6 @@
"La aplicacion sigue iniciada, pero la sesion de GlobalLeaks no esta activa. Introduce un nuevo 2FA para renovarla.",
"alert-warning");
}
-
- await TryRefreshLocalZipListAsync();
}
private async Task LoadSessionStateAsync()
@@ -457,7 +403,6 @@
}
}
- await RefreshLocalZipListAsync();
SelectedIds.Clear();
await LoadReportsAsync();
@@ -497,55 +442,6 @@
}
}
- private async Task ProcessLocalZipsAsync()
- {
- LocalBusy = true;
- try
- {
- var result = await ApiDenuncias.ProcessLocalZipsAsync();
- await RefreshLocalZipListAsync();
-
- if (result.TotalCandidates == 0)
- {
- SetStatus("No hay ZIPs pendientes en la carpeta local.", "alert-info");
- return;
- }
-
- SetStatus(
- result.Errors.Count == 0
- ? $"Se han procesado {result.ImportedCount} ZIP(s) de la carpeta local."
- : $"Se han procesado {result.ImportedCount} ZIP(s) con incidencias: {string.Join(" | ", result.Errors)}",
- result.Errors.Count == 0 ? "alert-success" : "alert-warning");
- }
- finally
- {
- LocalBusy = false;
- }
- }
-
- private async Task DeleteZipAsync(string zipName)
- {
- await ApiDenuncias.DeleteZipAsync(zipName);
- await RefreshLocalZipListAsync();
- }
-
- private async Task TryRefreshLocalZipListAsync()
- {
- try
- {
- await RefreshLocalZipListAsync();
- }
- catch
- {
- ExistingZips = Array.Empty();
- }
- }
-
- private async Task RefreshLocalZipListAsync()
- {
- ExistingZips = await ApiDenuncias.GetExistingZipNamesAsync();
- }
-
private void ApplyFilters()
{
IEnumerable filtered = Reports;
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Gestiona.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Gestiona.razor
index 48a3719..2b79dcb 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Gestiona.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Gestiona.razor
@@ -1,4 +1,4 @@
-@page "/Gestiona"
+@page "/Gestiona"
@rendermode InteractiveServer
@attribute [Authorize]
@using GestionaDenunciasAN.Models
@@ -11,7 +11,7 @@
@inject IDenunciaStore DenunciaStore
@inject ApiDenunciasClient ApiDenuncias
-Denuncias Gestión
+Denuncias Gestin
-Denuncias en Gestión
+Denuncias en Gestin
-
+
No hay denuncias en gestión.
+ No hay denuncias en gestin.
}
else
{
@@ -138,12 +138,12 @@ else
}
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
{
- Nº expediente Gestiona
+ N expediente Gestiona
@denuncia.ExpedienteGestionaMostrable
}
@if (denuncia.Id_Persona_Gestiona != 0)
{
- ID Persona Gestión
+ ID Persona Gestin
@denuncia.Id_Persona_Gestiona
}
@if (!string.IsNullOrWhiteSpace(denuncia.Etiqueta))
@@ -192,13 +192,13 @@ else
@denuncia.Asunto
A Quien Denuncia
@denuncia.A_Quien_Denuncia
- Descripción Denuncia
+ Descripcin Denuncia
@denuncia.Descripcion_Denuncia
Denunciado Ante Inst
@denuncia.Denunciado_Ante_Inst
@if (!string.IsNullOrWhiteSpace(denuncia.Modalidad_Informacion))
{
- Modalidad Información
+ Modalidad Informacin
@denuncia.Modalidad_Informacion
}
Lugar Hechos
@@ -210,27 +210,27 @@ else
}
-
- Datos de Notificación
+
+ Datos de Notificacin
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Preferencia))
{
- Notificación Preferencia
+ Notificacin Preferencia
@denuncia.Notificacion_Preferencia
}
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Electronica))
{
- Notificación Electrónica
+ Notificacin Electrnica
@denuncia.Notificacion_Electronica
}
@if (!string.IsNullOrWhiteSpace(denuncia.Correo_Electronico))
{
- Correo Electrónico
+ Correo Electrnico
@denuncia.Correo_Electronico
}
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Sms))
{
- Notificación SMS
+ Notificacin SMS
@denuncia.Notificacion_Sms
}
@@ -241,7 +241,7 @@ else
@if (denuncia.Condiciones)
{
Condiciones
- Sí
+ S
}
@if (!string.IsNullOrWhiteSpace(denuncia.Comments))
{
@@ -258,7 +258,7 @@ else
Nombre
- Tamaño (bytes)
+ Tamao (bytes)
Ver
@@ -309,7 +309,7 @@ else
private List denunciasGestiona = new();
private Dictionary> ficherosAdjuntos = new();
- // Variable para la búsqueda
+ // Variable para la bsqueda
private string busqueda = "";
private bool hasLoaded = false;
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Instrucciones.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Instrucciones.razor
index b01eb99..98537d5 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Instrucciones.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Instrucciones.razor
@@ -1,4 +1,4 @@
-@page "/Instrucciones"
+@page "/Instrucciones"
@attribute [Authorize]
@attribute [StreamRendering]
@inject GestionaDenunciasAN.Models.UserState userState
@@ -7,22 +7,22 @@
Instrucciones
-
Guía de Uso — Gestión de Denuncias
+
Gua de Uso Gestin de Denuncias
- Esta aplicación permite procesar denuncias desde archivos ZIP y gestionarlas en tres etapas:
- Pendientes , Gestión (aceptadas) y Rechazadas .
+ Esta aplicacin permite procesar denuncias desde archivos ZIP y gestionarlas en tres etapas:
+ Pendientes , Gestin (aceptadas) y Rechazadas .
1. Carga de ZIPs
- Sitúate en la pestaña Gestión de ZIP . Haz clic en Subir nuevo ZIP ,
+ Sitate en la pestaa Gestin de ZIP . Haz clic en Subir nuevo ZIP ,
selecciona uno o varios archivos .zip y espera a que se extraigan.
Cada ZIP debe incluir un report.txt con los campos de la denuncia, y opcionalmente
- subcarpetas files o files_attached_from_recipients con PDF e imágenes.
+ subcarpetas files o files_attached_from_recipients con PDF e imgenes.
Tras el procesado, la app lee los report.txt y actualiza la base de datos:
@@ -32,10 +32,10 @@
-
2. Pestaña Pendientes
+
2. Pestaa Pendientes
- Verás cada denuncia en una tarjeta colapsable con sus datos y el listado de ficheros adjuntos.
+ Vers cada denuncia en una tarjeta colapsable con sus datos y el listado de ficheros adjuntos.
Hay dos acciones:
@@ -47,30 +47,30 @@
Elegir el modo de subida:
- Unir todos los ficheros en un único PDF.
+ Unir todos los ficheros en un nico PDF.
Subir cada fichero de forma independiente.
Seleccionar el grupo de destino (600, 510 o 700).
- Confirmar. La denuncia se crea y abre en Gestióna, sube los documentos
- y pasa a la pestaña Gestión .
+ Confirmar. La denuncia se crea y abre en Gestina, sube los documentos
+ y pasa a la pestaa Gestin .
Rechazar denuncia (rojo): abre un modal para poner el motivo.
- Al confirmar, la denuncia se marca como rechazada y va a la pestaña
+ Al confirmar, la denuncia se marca como rechazada y va a la pestaa
Rechazados .
-
3. Pestaña Gestión
+
3. Pestaa Gestin
- Aquí se listan las denuncias que ya han sido enviadas a Gestión .
+ Aqu se listan las denuncias que ya han sido enviadas a Gestin .
Aparecen con fondo verde.
@@ -78,34 +78,34 @@
ID, nombre, archivo subido
Fecha y hora de subida
- Detalles completos y enlaces “Ver” a los PDFs/imágenes
+ Detalles completos y enlaces Ver a los PDFs/imgenes
-
4. Pestaña Rechazadas
+
4. Pestaa Rechazadas
- Aquí verás todas las denuncias que han sido rechazadas. Fondo rojo.
+ Aqu vers todas las denuncias que han sido rechazadas. Fondo rojo.
- Cada tarjeta muestra el motivo de rechazo y la fecha/hora en que se marcó.
+ Cada tarjeta muestra el motivo de rechazo y la fecha/hora en que se marc.
5. Flujo completo
- Subes uno o varios ZIP en la pestaña Gestión de ZIP .
- La aplicación extrae y parsea informes, los añade a Pendientes .
+ Subes uno o varios ZIP en la pestaa Gestin de ZIP .
+ La aplicacin extrae y parsea informes, los aade a Pendientes .
- En Pendientes eliges qué hacer con cada denuncia:
+ En Pendientes eliges qu hacer con cada denuncia:
- Configurar subida → pasa a Gestión .
- Rechazar denuncia → pasa a Rechazadas .
+ Configurar subida ? pasa a Gestin .
+ Rechazar denuncia ? pasa a Rechazadas .
- En Gestión puedes revisar lo ya subido; en
+ En Gestin puedes revisar lo ya subido; en
Rechazadas ves los motivos.
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Pendientes.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Pendientes.razor
index 5fbef1c..316c9e6 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Pendientes.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Pendientes.razor
@@ -2,13 +2,13 @@
@rendermode InteractiveServer
@attribute [Authorize]
-@using GestionaDenunciasAN.Models
+@using GestionaDenuncias.Shared.Models
@using System.Globalization
@using System.IO
@using System.Linq
@using System.Text
-@using GestionaDenunciasAN.Helpers
+@using GestionaDenuncias.Shared.Helpers
@using GestionaDenunciasAN.Services
@attribute [StreamRendering]
@@ -175,6 +175,11 @@
Denuncias Pendientes
+@if (!string.IsNullOrWhiteSpace(loadError))
+{
+
@loadError
+}
+
preselectedFicheros = new();
private bool hasLoaded = false;
+ private string loadError = string.Empty;
private bool showModal = false;
private bool showModalRechazo = false;
@@ -876,27 +882,37 @@ else
private async Task CargarDatosAsync()
{
- var todas = await CargarDenunciasJsonAsync();
-
- // Asegura ProcedureId/GroupId por si faltan
- foreach (var d in todas.Where(x => x.ProcedureId == Guid.Empty))
+ try
{
- d.ProcedureId = Guid.Parse("82722c9b-cecc-4299-8a7b-ce5abeb8170b");
- d.GroupId = Guid.Parse("6dbfc433-1eb6-4b9a-a533-bfebc652c101");
+ loadError = string.Empty;
+ var todas = await CargarDenunciasJsonAsync();
+
+ // Asegura ProcedureId/GroupId por si faltan
+ foreach (var d in todas.Where(x => x.ProcedureId == Guid.Empty))
+ {
+ d.ProcedureId = Guid.Parse("82722c9b-cecc-4299-8a7b-ce5abeb8170b");
+ d.GroupId = Guid.Parse("6dbfc433-1eb6-4b9a-a533-bfebc652c101");
+ }
+
+ // SOLO pendientes
+ pendientes = todas
+ .Where(d => !d.EnGestiona && !d.EnRechazada && !d.EsActualizacion)
+ .ToList();
+
+ // Adjuntos SOLO de las pendientes visibles
+ var listaF = await CargarFicherosJsonAsync();
+ var idsPend = pendientes.Select(p => p.Id_Denuncia).ToHashSet();
+ ficherosAdjuntos = listaF
+ .Where(f => idsPend.Contains(f.Id_Denuncia))
+ .GroupBy(f => f.Id_Denuncia)
+ .ToDictionary(g => g.Key, g => g.ToList());
+ }
+ catch (Exception ex)
+ {
+ pendientes.Clear();
+ ficherosAdjuntos.Clear();
+ loadError = $"No se han podido cargar las denuncias pendientes: {ex.Message}";
}
-
- // SOLO pendientes
- pendientes = todas
- .Where(d => !d.EnGestiona && !d.EnRechazada && !d.EsActualizacion)
- .ToList();
-
- // Adjuntos SOLO de las pendientes visibles
- var listaF = await CargarFicherosJsonAsync();
- var idsPend = pendientes.Select(p => p.Id_Denuncia).ToHashSet();
- ficherosAdjuntos = listaF
- .Where(f => idsPend.Contains(f.Id_Denuncia))
- .GroupBy(f => f.Id_Denuncia)
- .ToDictionary(g => g.Key, g => g.ToList());
hasLoaded = true;
StateHasChanged();
@@ -912,6 +928,19 @@ else
return await DenunciaStore.GetAllFicherosAsync();
}
+ private static bool IsReportFileName(string? fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ return false;
+ }
+
+ var name = Path.GetFileNameWithoutExtension(fileName);
+ var extension = Path.GetExtension(fileName);
+ return name.StartsWith("report", StringComparison.OrdinalIgnoreCase) &&
+ (extension.Equals(".txt", StringComparison.OrdinalIgnoreCase) ||
+ extension.Equals(".pdf", StringComparison.OrdinalIgnoreCase));
+ }
private string FixFileName(string input)
{
var normalized = input.Normalize(NormalizationForm.FormD);
@@ -995,7 +1024,7 @@ else
string? documentoParaTramitar = null;
var report = todos.FirstOrDefault(t =>
- string.Equals(t.FileName, reportTxt, StringComparison.OrdinalIgnoreCase));
+ IsReportFileName(t.FileName));
if (!string.IsNullOrWhiteSpace(report.FileName))
{
@@ -1013,7 +1042,7 @@ else
}
var adjuntos = todos
- .Where(t => !string.Equals(t.FileName, reportTxt, StringComparison.OrdinalIgnoreCase))
+ .Where(t => !IsReportFileName(t.FileName))
.ToList();
if (adjuntos.Count > 0 && uploadMode == "merge")
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Rechazados.razor b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Rechazados.razor
index eee87f8..06c821b 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Rechazados.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/Pages/Rechazados.razor
@@ -1,4 +1,4 @@
-@page "/Rechazados"
+@page "/Rechazados"
@rendermode InteractiveServer
@attribute [Authorize]
@using GestionaDenunciasAN.Models
@@ -62,7 +62,7 @@
.card-body {
padding: 1.25rem;
}
- /* Estilos para los títulos de sección dentro de la card */
+ /* Estilos para los ttulos de seccin dentro de la card */
.section-heading {
text-align: center;
font-weight: bold;
@@ -75,7 +75,7 @@
Denuncias Rechazadas
-
+
Nº expediente Gestiona
+
N expediente Gestiona
@denuncia.ExpedienteGestionaMostrable
}
@if (denuncia.Id_Persona_Gestiona != 0)
{
-
ID Persona Gestión
+
ID Persona Gestin
@denuncia.Id_Persona_Gestiona
}
@if (!string.IsNullOrWhiteSpace(denuncia.Etiqueta))
@@ -187,13 +187,13 @@ else
@denuncia.Asunto
A Quien Denuncia
@denuncia.A_Quien_Denuncia
-
Descripción Denuncia
+
Descripcin Denuncia
@denuncia.Descripcion_Denuncia
Denunciado Ante Inst
@denuncia.Denunciado_Ante_Inst
@if (!string.IsNullOrWhiteSpace(denuncia.Modalidad_Informacion))
{
-
Modalidad Información
+
Modalidad Informacin
@denuncia.Modalidad_Informacion
}
Lugar Hechos
@@ -205,27 +205,27 @@ else
}
-
-
Datos de Notificación
+
+
Datos de Notificacin
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Preferencia))
{
- Notificación Preferencia
+ Notificacin Preferencia
@denuncia.Notificacion_Preferencia
}
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Electronica))
{
- Notificación Electrónica
+ Notificacin Electrnica
@denuncia.Notificacion_Electronica
}
@if (!string.IsNullOrWhiteSpace(denuncia.Correo_Electronico))
{
- Correo Electrónico
+ Correo Electrnico
@denuncia.Correo_Electronico
}
@if (!string.IsNullOrWhiteSpace(denuncia.Notificacion_Sms))
{
- Notificación SMS
+ Notificacin SMS
@denuncia.Notificacion_Sms
}
@@ -236,7 +236,7 @@ else
@if (denuncia.Condiciones)
{
Condiciones
-
Sí
+
S
}
@if (!string.IsNullOrWhiteSpace(denuncia.Comments))
{
@@ -253,7 +253,7 @@ else
Nombre
- Tamaño (bytes)
+ Tamao (bytes)
Ver
@@ -304,7 +304,7 @@ else
private List
denunciasRechazadas = new();
private Dictionary> ficherosAdjuntos = new();
- // Variable para la búsqueda
+ // Variable para la bsqueda
private string busqueda = "";
private bool hasLoaded = false;
diff --git a/Antifraude.Net/GestionaDenunciasAN/Components/_Imports.razor b/Antifraude.Net/GestionaDenunciasAN/Components/_Imports.razor
index c31d0da..6200830 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Components/_Imports.razor
+++ b/Antifraude.Net/GestionaDenunciasAN/Components/_Imports.razor
@@ -11,5 +11,7 @@
@using GestionaDenunciasAN
@using GestionaDenunciasAN.Components
@using GestionaDenunciasAN.Components.Layout
+@using GestionaDenuncias.Shared.Models
+@using GestionaDenuncias.Shared.Services
@using GestionaDenunciasAN.Models
@using GestionaDenunciasAN.Services
diff --git a/Antifraude.Net/GestionaDenunciasAN/Configuration/ComplaintStorageOptions.cs b/Antifraude.Net/GestionaDenunciasAN/Configuration/ComplaintStorageOptions.cs
deleted file mode 100644
index b40b83d..0000000
--- a/Antifraude.Net/GestionaDenunciasAN/Configuration/ComplaintStorageOptions.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace GestionaDenunciasAN.Configuration;
-
-public sealed class ComplaintStorageOptions
-{
- public const string SectionName = "ComplaintStorage";
-
- public string ConnectionString { get; set; } = string.Empty;
- public bool AutoCreateSchema { get; set; }
-}
diff --git a/Antifraude.Net/GestionaDenunciasAN/GestionaDenunciasAN.csproj b/Antifraude.Net/GestionaDenunciasAN/GestionaDenunciasAN.csproj
index 5caff16..8340d25 100644
--- a/Antifraude.Net/GestionaDenunciasAN/GestionaDenunciasAN.csproj
+++ b/Antifraude.Net/GestionaDenunciasAN/GestionaDenunciasAN.csproj
@@ -7,13 +7,7 @@
-
-
-
-
-
-
-
+
diff --git a/Antifraude.Net/GestionaDenunciasAN/GlobalUsings.cs b/Antifraude.Net/GestionaDenunciasAN/GlobalUsings.cs
new file mode 100644
index 0000000..8725d4e
--- /dev/null
+++ b/Antifraude.Net/GestionaDenunciasAN/GlobalUsings.cs
@@ -0,0 +1,2 @@
+global using GestionaDenuncias.Shared.Models;
+global using GestionaDenuncias.Shared.Services;
diff --git a/Antifraude.Net/GestionaDenunciasAN/Models/UserState.cs b/Antifraude.Net/GestionaDenunciasAN/Models/UserState.cs
index b951653..2e72593 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Models/UserState.cs
+++ b/Antifraude.Net/GestionaDenunciasAN/Models/UserState.cs
@@ -1,4 +1,4 @@
-namespace GestionaDenunciasAN.Models
+namespace GestionaDenunciasAN.Models
{
public class UserState
{
diff --git a/Antifraude.Net/GestionaDenunciasAN/Program.cs b/Antifraude.Net/GestionaDenunciasAN/Program.cs
index 4988214..5894539 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Program.cs
+++ b/Antifraude.Net/GestionaDenunciasAN/Program.cs
@@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
using GestionaDenunciasAN.Components;
using GestionaDenunciasAN.Configuration;
using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
using GestionaDenunciasAN.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
@@ -71,7 +72,10 @@ app.Use(async (context, next) =>
await next();
});
-app.UseHttpsRedirection();
+if (builder.Configuration.GetValue("ForceHttpsRedirection", false))
+{
+ app.UseHttpsRedirection();
+}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/ApiDenunciaStore.cs b/Antifraude.Net/GestionaDenunciasAN/Services/ApiDenunciaStore.cs
index 7221be5..5c96269 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/ApiDenunciaStore.cs
+++ b/Antifraude.Net/GestionaDenunciasAN/Services/ApiDenunciaStore.cs
@@ -1,4 +1,4 @@
-using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
namespace GestionaDenunciasAN.Services;
@@ -16,7 +16,6 @@ public sealed class ApiDenunciaStore : IDenunciaStore
public async Task> GetAllDenunciasAsync(CancellationToken cancellationToken = default)
=> (await _api.GetAsync>("api/denuncias", cancellationToken)) ?? [];
-
public async Task> GetAllFicherosAsync(CancellationToken cancellationToken = default)
=> (await _api.GetAsync>("api/denuncias/ficheros", cancellationToken)) ?? [];
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/ApiDenunciasClient.cs b/Antifraude.Net/GestionaDenunciasAN/Services/ApiDenunciasClient.cs
index ac698c4..8ea3e69 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/ApiDenunciasClient.cs
+++ b/Antifraude.Net/GestionaDenunciasAN/Services/ApiDenunciasClient.cs
@@ -2,7 +2,7 @@ using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
-using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
using Microsoft.AspNetCore.Components.Authorization;
namespace GestionaDenunciasAN.Services;
@@ -18,15 +18,18 @@ public sealed class ApiDenunciasClient
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly AuthenticationStateProvider _authenticationStateProvider;
+ private readonly ILogger _logger;
public ApiDenunciasClient(
IHttpClientFactory httpClientFactory,
IHttpContextAccessor httpContextAccessor,
- AuthenticationStateProvider authenticationStateProvider)
+ AuthenticationStateProvider authenticationStateProvider,
+ ILogger logger)
{
_httpClientFactory = httpClientFactory;
_httpContextAccessor = httpContextAccessor;
_authenticationStateProvider = authenticationStateProvider;
+ _logger = logger;
}
public Task LoginAsync(LoginRequest request, CancellationToken cancellationToken = default)
@@ -212,16 +215,26 @@ public sealed class ApiDenunciasClient
var token = await GetAccessTokenAsync();
if (string.IsNullOrWhiteSpace(token))
{
+ _logger.LogWarning("No hay token de API disponible para llamar a {Path}. Usuario autenticado={IsAuthenticated}",
+ path,
+ _httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated);
throw new UnauthorizedAccessException("No hay token de API activo. Vuelve a iniciar sesion.");
}
+ _logger.LogInformation("Llamando a API protegida {Path}. Token presente={TokenPresent}", path, true);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
using var response = await client.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
- throw new UnauthorizedAccessException("La sesion de API ha caducado. Vuelve a iniciar sesion.");
+ if (authorize)
+ {
+ throw new UnauthorizedAccessException($"La sesion de API ha caducado al llamar a {path}. Vuelve a iniciar sesion.");
+ }
+
+ var message = await ReadErrorMessageAsync(response, cancellationToken);
+ throw new UnauthorizedAccessException(message);
}
if (!response.IsSuccessStatusCode)
diff --git a/Antifraude.Net/GestionaDenunciasAN/Services/ApiInboxTrackingService.cs b/Antifraude.Net/GestionaDenunciasAN/Services/ApiInboxTrackingService.cs
index eee0a0e..db166ca 100644
--- a/Antifraude.Net/GestionaDenunciasAN/Services/ApiInboxTrackingService.cs
+++ b/Antifraude.Net/GestionaDenunciasAN/Services/ApiInboxTrackingService.cs
@@ -1,4 +1,4 @@
-using GestionaDenunciasAN.Models;
+using GestionaDenuncias.Shared.Models;
namespace GestionaDenunciasAN.Services;
@@ -32,4 +32,13 @@ public sealed class ApiInboxTrackingService : IInboxTrackingService
"api/tracking/imported",
new MarkReportImportedRequest(username, report, complaintId),
cancellationToken);
+
+ public Task EnsureReportCanBeImportedByUserAsync(
+ string username,
+ ReportDto report,
+ CancellationToken cancellationToken = default)
+ => _api.PostAsync(
+ "api/tracking/import-permission",
+ new TrackingImportPermissionRequest(username, report),
+ cancellationToken);
}
diff --git a/Antifraude.Net/GestionaDenunciasAN/appsettings.json b/Antifraude.Net/GestionaDenunciasAN/appsettings.json
index a370dcf..719c97b 100644
--- a/Antifraude.Net/GestionaDenunciasAN/appsettings.json
+++ b/Antifraude.Net/GestionaDenunciasAN/appsettings.json
@@ -6,7 +6,8 @@
}
},
"AllowedHosts": "*",
+ "ForceHttpsRedirection": false,
"ApiDenuncias": {
- "BaseUrl": "https://localhost:7093"
+ "BaseUrl": "http://localhost:7093"
}
}