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))" />
@@ -90,7 +90,7 @@ {
- + 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
} @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

    -
  1. Subes uno o varios ZIP en la pestaña Gestión de ZIP.
  2. -
  3. La aplicación extrae y parsea informes, los añade a Pendientes.
  4. +
  5. Subes uno o varios ZIP en la pestaa Gestin de ZIP.
  6. +
  7. La aplicacin extrae y parsea informes, los aade a Pendientes.
  8. - 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.
  9. - 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
} @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" } }