cambios de denuncias
This commit is contained in:
@@ -9,10 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SwaggerAntifraude", "Swagge
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistroPersonalAN", "RegistroPersonalAN\RegistroPersonalAN.csproj", "{690BFF6A-F3FC-4D94-9E32-C689FBB69455}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistroPersonalAN", "RegistroPersonalAN\RegistroPersonalAN.csproj", "{690BFF6A-F3FC-4D94-9E32-C689FBB69455}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GestionaDenunciasAN", "GestionaDenunciasAN\GestionaDenunciasAN.csproj", "{27476EF0-284B-402C-ADBF-70A42220725F}"
|
|
||||||
EndProject
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GestionPersonalWeb", "GestionPersonalWeb\GestionPersonalWeb.csproj", "{063515F3-D202-45DD-91DA-A494FBD005AD}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GestionPersonalWeb", "GestionPersonalWeb\GestionPersonalWeb.csproj", "{063515F3-D202-45DD-91DA-A494FBD005AD}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GestionaDenunciasAN", "GestionaDenunciasAN\GestionaDenunciasAN.csproj", "{77BE75E1-E1FD-AAE7-D897-398BED72CEB1}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -31,14 +31,14 @@ Global
|
|||||||
{690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Debug|Any CPU.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.ActiveCfg = Release|Any CPU
|
||||||
{690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Release|Any CPU.Build.0 = Release|Any CPU
|
{690BFF6A-F3FC-4D94-9E32-C689FBB69455}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{27476EF0-284B-402C-ADBF-70A42220725F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{27476EF0-284B-402C-ADBF-70A42220725F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{27476EF0-284B-402C-ADBF-70A42220725F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{27476EF0-284B-402C-ADBF-70A42220725F}.Release|Any CPU.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.ActiveCfg = Debug|Any CPU
|
||||||
{063515F3-D202-45DD-91DA-A494FBD005AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{063515F3-D202-45DD-91DA-A494FBD005AD}.Debug|Any CPU.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.ActiveCfg = Release|Any CPU
|
||||||
{063515F3-D202-45DD-91DA-A494FBD005AD}.Release|Any CPU.Build.0 = Release|Any CPU
|
{063515F3-D202-45DD-91DA-A494FBD005AD}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{77BE75E1-E1FD-AAE7-D897-398BED72CEB1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -11,14 +11,16 @@
|
|||||||
<link rel="stylesheet" href="app.css" />
|
<link rel="stylesheet" href="app.css" />
|
||||||
<link rel="stylesheet" href="GestionaDenunciasAN.styles.css" />
|
<link rel="stylesheet" href="GestionaDenunciasAN.styles.css" />
|
||||||
<link rel="icon" type="image/png" href="favicon.png" />
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
<HeadOutlet />
|
<HeadOutlet @rendermode="@(new InteractiveServerRenderMode(prerender: false))" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<Routes />
|
<Routes @rendermode="@(new InteractiveServerRenderMode(prerender: false))" />
|
||||||
<script src="Scripts/bootstrap.bundle.min.js"></script>
|
<script src="Scripts/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="js/appAuth.js"></script>
|
||||||
|
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
|
<p class="m-4 text-muted">Redirigiendo al inicio...</p>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
protected override void OnAfterRender(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
var relativePath = Navigation.ToBaseRelativePath(Navigation.Uri);
|
||||||
|
var targetPath = string.IsNullOrWhiteSpace(relativePath)
|
||||||
|
? "/"
|
||||||
|
: $"/{relativePath}";
|
||||||
|
|
||||||
|
var loginUrl = targetPath == "/"
|
||||||
|
? "/"
|
||||||
|
: $"/?returnUrl={Uri.EscapeDataString(targetPath)}";
|
||||||
|
|
||||||
|
Navigation.NavigateTo(loginUrl, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,88 +1,40 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@implements IDisposable
|
||||||
@inject GestionaDenunciasAN.Models.UserState userState
|
@inject GestionaDenunciasAN.Models.UserState userState
|
||||||
|
@inject IHttpContextAccessor HttpContextAccessor
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
<style>
|
<div class="app-shell">
|
||||||
|
<aside class="app-sidebar">
|
||||||
|
|
||||||
/* Barra superior con gradiente de azul: izquierda azul clarito, derecha azul oscuro */
|
|
||||||
.top-row {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 60px;
|
|
||||||
background: linear-gradient(to right, #5a9bd5, #1f497d);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Título del portal posicionado a la izquierda */
|
|
||||||
.portal-title {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Contenedor del enlace de usuario posicionado a la derecha */
|
|
||||||
.logout-container {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enlace que luce como texto, sin decoraciones */
|
|
||||||
.logout-link {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #fff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
font: inherit;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Icono SVG más grande */
|
|
||||||
.logout-link img {
|
|
||||||
height: 1.5em;
|
|
||||||
width: auto;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Al pasar el ratón, se pone en negrita y mantiene el color blanco */
|
|
||||||
.logout-link:hover,
|
|
||||||
.logout-link:focus,
|
|
||||||
.logout-link:active {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #fff !important;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<div class="sidebar">
|
|
||||||
<NavMenu />
|
<NavMenu />
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="app-main">
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="app-header__intro">
|
||||||
|
<span class="app-header__eyebrow">Portal interno</span>
|
||||||
|
<h1 class="app-header__title">@CurrentPageTitle</h1>
|
||||||
|
<p class="app-header__copy mb-0">@CurrentPageDescription</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main>
|
<div class="app-header__actions">
|
||||||
<!-- Barra superior completa con gradiente de azul -->
|
<div class="app-session-pill">
|
||||||
<div class="top-row">
|
<span class="app-session-pill__dot"></span>
|
||||||
<div class="portal-title">
|
Sesion interna activa
|
||||||
Portal Gestion Denuncias
|
|
||||||
</div>
|
|
||||||
<div class="logout-container">
|
|
||||||
<a class="logout-link" href="/">
|
|
||||||
<img src="Content/icon/person-fill.svg" alt="User Icon" />
|
|
||||||
<span>@userState?.NombreUsu</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article class="content">
|
<button type="button" class="app-user-chip" @onclick="CerrarSesionAsync">
|
||||||
|
<span class="bi bi-person-circle app-user-chip__icon" aria-hidden="true"></span>
|
||||||
|
<span class="app-user-chip__text">
|
||||||
|
<strong>@DisplayUsername</strong>
|
||||||
|
<small>Cerrar sesion</small>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article class="app-content">
|
||||||
@Body
|
@Body
|
||||||
</article>
|
</article>
|
||||||
</main>
|
</main>
|
||||||
@@ -91,5 +43,106 @@
|
|||||||
<div id="blazor-error-ui">
|
<div id="blazor-error-ui">
|
||||||
An unhandled error has occurred.
|
An unhandled error has occurred.
|
||||||
<a href="" class="reload">Reload</a>
|
<a href="" class="reload">Reload</a>
|
||||||
<a class="dismiss">🗙</a>
|
<a class="dismiss">x</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string CurrentPageTitle { get; set; } = "Portal de gestion";
|
||||||
|
private string CurrentPageDescription { get; set; } =
|
||||||
|
"Entrada, revision y tramitacion coordinada de denuncias y actualizaciones.";
|
||||||
|
|
||||||
|
private string DisplayUsername =>
|
||||||
|
string.IsNullOrWhiteSpace(userState?.NombreUsu)
|
||||||
|
? "Usuario"
|
||||||
|
: userState.NombreUsu;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
Navigation.LocationChanged += HandleLocationChanged;
|
||||||
|
RefreshLayoutState();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
RefreshLayoutState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Navigation.LocationChanged -= HandleLocationChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLocationChanged(object? sender, LocationChangedEventArgs args)
|
||||||
|
{
|
||||||
|
RefreshLayoutState();
|
||||||
|
_ = InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshLayoutState()
|
||||||
|
{
|
||||||
|
SincronizarUsuario();
|
||||||
|
var pageInfo = ResolvePageInfo();
|
||||||
|
CurrentPageTitle = pageInfo.Title;
|
||||||
|
CurrentPageDescription = pageInfo.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (string Title, string Description) ResolvePageInfo()
|
||||||
|
{
|
||||||
|
var relative = Navigation.ToBaseRelativePath(Navigation.Uri);
|
||||||
|
var path = string.IsNullOrWhiteSpace(relative)
|
||||||
|
? string.Empty
|
||||||
|
: relative.Split('?', '#')[0].Trim('/');
|
||||||
|
|
||||||
|
return path.ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"" or "gestionzip" => (
|
||||||
|
"Entrada de denuncias",
|
||||||
|
"Importa lo nuevo desde GlobalLeaks, revisa el seguimiento por usuario y decide si habra expediente nuevo o actualizacion."),
|
||||||
|
"pendientes" => (
|
||||||
|
"Denuncias pendientes",
|
||||||
|
"Prepara expedientes nuevos con todos los datos del formulario, documentos y tercero ya resueltos."),
|
||||||
|
"actualizaciones" => (
|
||||||
|
"Actualizaciones",
|
||||||
|
"Gestiona ampliaciones sobre expedientes existentes y sube solo los adjuntos realmente nuevos."),
|
||||||
|
"gestiona" => (
|
||||||
|
"Expedientes en Gestiona",
|
||||||
|
"Consulta el estado de los expedientes ya enviados y continua su seguimiento operativo."),
|
||||||
|
"rechazados" => (
|
||||||
|
"Denuncias rechazadas",
|
||||||
|
"Mantiene trazabilidad de los descartes y de los motivos aplicados en la revision."),
|
||||||
|
"buscador" => (
|
||||||
|
"Buscador de terceros",
|
||||||
|
"Localiza terceros y expedientes relacionados para validar identidades antes de tramitar."),
|
||||||
|
"instrucciones" => (
|
||||||
|
"Instrucciones",
|
||||||
|
"Referencia rapida de uso para el equipo gestor y para las operaciones mas frecuentes."),
|
||||||
|
_ => (
|
||||||
|
"Portal de gestion",
|
||||||
|
"Entrada, revision y tramitacion coordinada de denuncias y actualizaciones.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SincronizarUsuario()
|
||||||
|
{
|
||||||
|
var user = HttpContextAccessor.HttpContext?.User;
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isAuthenticated = user.Identity?.IsAuthenticated == true;
|
||||||
|
|
||||||
|
userState.Token = isAuthenticated ? "authenticated" : string.Empty;
|
||||||
|
userState.NombreUsu = isAuthenticated
|
||||||
|
? (user.Identity?.Name ?? string.Empty)
|
||||||
|
: string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CerrarSesionAsync()
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeAsync<object>("appAuthPost", "/api/auth/logout");
|
||||||
|
userState.Token = string.Empty;
|
||||||
|
userState.NombreUsu = string.Empty;
|
||||||
|
Navigation.NavigateTo("/", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,96 +1 @@
|
|||||||
.page {
|
/* Layout styles moved to wwwroot/app.css for a shared app-wide theme. */
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row {
|
|
||||||
background-color: #f7f7f7;
|
|
||||||
border-bottom: 1px solid #d6d5d5;
|
|
||||||
justify-content: flex-end;
|
|
||||||
height: 3.5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a:first-child {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640.98px) {
|
|
||||||
.top-row {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
|
||||||
.page {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
height: 100vh;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row.auth ::deep a:first-child {
|
|
||||||
flex: 1;
|
|
||||||
text-align: right;
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row, article {
|
|
||||||
padding-left: 2rem !important;
|
|
||||||
padding-right: 1.5rem !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#blazor-error-ui {
|
|
||||||
background: lightyellow;
|
|
||||||
bottom: 0;
|
|
||||||
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
|
||||||
display: none;
|
|
||||||
left: 0;
|
|
||||||
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#blazor-error-ui .dismiss {
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
right: 0.75rem;
|
|
||||||
top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,99 +1,82 @@
|
|||||||
<!-- NavMenu.razor -->
|
<div class="nav-shell">
|
||||||
<style>
|
<div class="nav-brand">
|
||||||
.nav-menu-container {
|
<img class="nav-brand__logo"
|
||||||
height: 100vh;
|
src="Content/imagenes/logo-oaaf-negativo-transparente.svg"
|
||||||
overflow: hidden;
|
alt="Logo Oficina Antifraude" />
|
||||||
}
|
<div class="nav-brand__copy">
|
||||||
|
<span class="nav-brand__eyebrow">OAAF</span>
|
||||||
/* Parte superior con el logo */
|
<strong class="nav-brand__title">Gestion de denuncias</strong>
|
||||||
.nav-top {
|
<span class="nav-brand__subtitle">Entrada, revision y tramitacion unificada</span>
|
||||||
background-color: #5a9bd5 !important;
|
|
||||||
padding: 0.5rem;
|
|
||||||
height: 5em;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-top img {
|
|
||||||
height: 4em;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sección scrollable para el contenido del menú */
|
|
||||||
.nav-scrollable {
|
|
||||||
background: linear-gradient(to bottom, #5a9bd5, #1f497d);
|
|
||||||
height: calc(100vh - 5em);
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empujar el grupo inferior (Instrucciones) al fondo, con un margen inferior */
|
|
||||||
.bottom-group {
|
|
||||||
margin-top: auto;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ajuste de los enlaces para icono y texto en la misma línea */
|
|
||||||
.nav-link {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link .bi {
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
line-height: 1;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="nav-menu-container">
|
|
||||||
<!-- Parte superior: logo -->
|
|
||||||
<div class="nav-top">
|
|
||||||
<div class="container-fluid" style="display: flex; justify-content: center; align-items: center;">
|
|
||||||
<img src="Content/imagenes/logo-oaaf-negativo-transparente.svg" alt="logo" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contenido scrollable: menú de navegación -->
|
<nav class="nav-sections" aria-label="Navegacion principal">
|
||||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
<div class="nav-section">
|
||||||
<nav class="flex-column" style="display: flex; height: 100%;">
|
<span class="nav-section__label">Operativa diaria</span>
|
||||||
<!-- Grupo superior: Pendientes y Finalizados -->
|
|
||||||
<div>
|
<NavLink class="menu-link" href="/GestionZip" Match="NavLinkMatch.All">
|
||||||
<div class="nav-item px-3">
|
<span class="menu-link__icon bi bi-box-seam" aria-hidden="true"></span>
|
||||||
<NavLink class="nav-link" href="GestionZip" Match="NavLinkMatch.All">
|
<span class="menu-link__content">
|
||||||
<span class="bi bi-list-task" aria-hidden="true"></span> Gestion ZIP
|
<span class="menu-link__title">Entrada</span>
|
||||||
|
<span class="menu-link__meta">Importar denuncias y renovar 2FA</span>
|
||||||
|
</span>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink class="menu-link" href="/Pendientes" Match="NavLinkMatch.All">
|
||||||
|
<span class="menu-link__icon bi bi-inbox" aria-hidden="true"></span>
|
||||||
|
<span class="menu-link__content">
|
||||||
|
<span class="menu-link__title">Pendientes</span>
|
||||||
|
<span class="menu-link__meta">Expedientes nuevos listos para tramitar</span>
|
||||||
|
</span>
|
||||||
|
</NavLink>
|
||||||
|
|
||||||
|
<NavLink class="menu-link" href="/Actualizaciones" Match="NavLinkMatch.All">
|
||||||
|
<span class="menu-link__icon bi bi-arrow-repeat" aria-hidden="true"></span>
|
||||||
|
<span class="menu-link__content">
|
||||||
|
<span class="menu-link__title">Actualizaciones</span>
|
||||||
|
<span class="menu-link__meta">Nuevos documentos sobre expedientes ya abiertos</span>
|
||||||
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-item px-3">
|
<div class="nav-section">
|
||||||
<NavLink class="nav-link" href="Pendientes" Match="NavLinkMatch.All">
|
<span class="nav-section__label">Consulta y control</span>
|
||||||
<span class="bi bi-list-task" aria-hidden="true"></span> Pendientes
|
|
||||||
|
<NavLink class="menu-link" href="/Gestiona" Match="NavLinkMatch.All">
|
||||||
|
<span class="menu-link__icon bi bi-journal-check" aria-hidden="true"></span>
|
||||||
|
<span class="menu-link__content">
|
||||||
|
<span class="menu-link__title">Gestiona</span>
|
||||||
|
<span class="menu-link__meta">Seguimiento de expedientes enviados</span>
|
||||||
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
|
||||||
<div class="nav-item px-3">
|
<NavLink class="menu-link" href="/Rechazados" Match="NavLinkMatch.All">
|
||||||
<NavLink class="nav-link" href="Gestiona" Match="NavLinkMatch.All">
|
<span class="menu-link__icon bi bi-journal-x" aria-hidden="true"></span>
|
||||||
<span class="bi bi-journal-check" aria-hidden="true"></span> Gestiona
|
<span class="menu-link__content">
|
||||||
|
<span class="menu-link__title">Rechazados</span>
|
||||||
|
<span class="menu-link__meta">Historico de descartes y motivos</span>
|
||||||
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
|
||||||
<div class="nav-item px-3">
|
<NavLink class="menu-link" href="/Buscador" Match="NavLinkMatch.All">
|
||||||
<NavLink class="nav-link" href="Rechazados" Match="NavLinkMatch.All">
|
<span class="menu-link__icon bi bi-search" aria-hidden="true"></span>
|
||||||
<span class="bi bi-journal-x" aria-hidden="true"></span> Rechazados
|
<span class="menu-link__content">
|
||||||
|
<span class="menu-link__title">Buscador de terceros</span>
|
||||||
|
<span class="menu-link__meta">Consulta identidades y expedientes vinculados</span>
|
||||||
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Grupo inferior: Instrucciones, empujado al fondo -->
|
<div class="nav-section nav-section--footer">
|
||||||
<div class="bottom-group">
|
<span class="nav-section__label">Ayuda</span>
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="Instrucciones">
|
<NavLink class="menu-link" href="/Instrucciones" Match="NavLinkMatch.All">
|
||||||
<span class="bi bi-book-half" aria-hidden="true"></span> Instrucciones
|
<span class="menu-link__icon bi bi-book-half" aria-hidden="true"></span>
|
||||||
|
<span class="menu-link__content">
|
||||||
|
<span class="menu-link__title">Instrucciones</span>
|
||||||
|
<span class="menu-link__meta">Guia rapida para el equipo gestor</span>
|
||||||
|
</span>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,105 +1 @@
|
|||||||
.navbar-toggler {
|
/* Navigation styles moved to wwwroot/app.css for a shared app-wide theme. */
|
||||||
appearance: none;
|
|
||||||
cursor: pointer;
|
|
||||||
width: 3.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
color: white;
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 1rem;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-toggler:checked {
|
|
||||||
background-color: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-row {
|
|
||||||
height: 3.5rem;
|
|
||||||
background-color: rgba(0,0,0,0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bi {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
top: -1px;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bi-house-door-fill-nav-menu {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.bi-plus-square-fill-nav-menu {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.bi-list-nested-nav-menu {
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item:first-of-type {
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item:last-of-type {
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item ::deep .nav-link {
|
|
||||||
color: #d7d7d7;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
height: 3rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
line-height: 3rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item ::deep a.active {
|
|
||||||
background-color: rgba(255,255,255,0.37);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-item ::deep .nav-link:hover {
|
|
||||||
background-color: rgba(255,255,255,0.1);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-scrollable {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-toggler:checked ~ .nav-scrollable {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 641px) {
|
|
||||||
.navbar-toggler {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-scrollable {
|
|
||||||
/* Never collapse the sidebar for wide screens */
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
/* Allow sidebar to scroll for tall menus */
|
|
||||||
height: calc(100vh - 3.5rem);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,593 @@
|
|||||||
|
@page "/Buscador"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
@attribute [Authorize]
|
||||||
|
|
||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Headers
|
||||||
|
@using System.Text
|
||||||
|
@using System.Text.Json
|
||||||
|
@using System.Linq
|
||||||
|
@using GestionaDenunciasAN.Models
|
||||||
|
|
||||||
|
@inject HttpClient Http
|
||||||
|
@inject IConfiguration Configuration
|
||||||
|
|
||||||
|
<h3>Buscador de expedientes por tercero</h3>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- NIF -->
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">DNI / NIF</label>
|
||||||
|
<input class="form-control"
|
||||||
|
placeholder="Ej.: 12345678Z"
|
||||||
|
@bind="nifBuscado"
|
||||||
|
@bind:event="oninput" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<!-- Modo de fechas -->
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label d-block">Rango de fechas</label>
|
||||||
|
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
id="modoTodas"
|
||||||
|
name="modoFecha"
|
||||||
|
checked="@IsModo(ModoTodas)"
|
||||||
|
@onchange="@(() => SetModo(ModoTodas))" />
|
||||||
|
<label class="form-check-label" for="modoTodas">
|
||||||
|
Todas las fechas
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
id="modoRango"
|
||||||
|
name="modoFecha"
|
||||||
|
checked="@IsModo(ModoRango)"
|
||||||
|
@onchange="@(() => SetModo(ModoRango))" />
|
||||||
|
<label class="form-check-label" for="modoRango">
|
||||||
|
Entre fechas
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input"
|
||||||
|
type="radio"
|
||||||
|
id="modoUltimos"
|
||||||
|
name="modoFecha"
|
||||||
|
checked="@IsModo(ModoUltimos)"
|
||||||
|
@onchange="@(() => SetModo(ModoUltimos))" />
|
||||||
|
<label class="form-check-label" for="modoUltimos">
|
||||||
|
Últimos X meses
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rango de fechas -->
|
||||||
|
@if (modoFecha == ModoRango)
|
||||||
|
{
|
||||||
|
<div class="row g-2 mt-2">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Desde</label>
|
||||||
|
<input class="form-control"
|
||||||
|
type="date"
|
||||||
|
@bind="fechaDesde" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Hasta</label>
|
||||||
|
<input class="form-control"
|
||||||
|
type="date"
|
||||||
|
@bind="fechaHasta" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (modoFecha == ModoUltimos)
|
||||||
|
{
|
||||||
|
<div class="row g-2 mt-2">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Últimos meses</label>
|
||||||
|
<select class="form-select" @bind="mesesUltimos">
|
||||||
|
<option value="3">3 meses</option>
|
||||||
|
<option value="6">6 meses</option>
|
||||||
|
<option value="9">9 meses</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Botón buscar -->
|
||||||
|
<div class="row g-2 mt-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
@onclick="BuscarAsync"
|
||||||
|
disabled="@isSearching">
|
||||||
|
@if (isSearching)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
<span class="ms-2">Buscando…</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
<span class="ms-1">Buscar</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger mt-3">@errorMessage</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (haBuscado)
|
||||||
|
{
|
||||||
|
<div class="mt-4">
|
||||||
|
<h5>Resultados para <strong>@nifMostrado</strong></h5>
|
||||||
|
|
||||||
|
@if (expedientes == null || !expedientes.Any())
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning mt-3">
|
||||||
|
No se han encontrado expedientes asociados a este tercero con los filtros seleccionados.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<table class="table table-striped table-hover mt-3">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Expediente</th>
|
||||||
|
<th>Asunto</th>
|
||||||
|
<th>Fecha creación</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var exp in expedientes)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@exp.CodigoExpediente</td>
|
||||||
|
<td>@exp.Asunto</td>
|
||||||
|
<td>@(exp.FechaCreacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm"))</td>
|
||||||
|
<td>@exp.Estado</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button class="btn btn-sm btn-outline-primary"
|
||||||
|
@onclick="() => ToggleDetalle(exp)">
|
||||||
|
@(expedienteSeleccionado == exp ? "Ocultar" : "Abrir expediente")
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
@if (expedienteSeleccionado == exp)
|
||||||
|
{
|
||||||
|
<tr class="table-active">
|
||||||
|
<td colspan="5">
|
||||||
|
<dl class="row mb-0">
|
||||||
|
<dt class="col-sm-2">Expediente</dt>
|
||||||
|
<dd class="col-sm-10">@exp.CodigoExpediente</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-2">Asunto</dt>
|
||||||
|
<dd class="col-sm-10">@exp.Asunto</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-2">Fecha creación</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
@exp.FechaCreacion?.ToLocalTime().ToString("dd/MM/yyyy HH:mm")
|
||||||
|
</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-2">Estado</dt>
|
||||||
|
<dd class="col-sm-10">@exp.Estado</dd>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(exp.FileUrl))
|
||||||
|
{
|
||||||
|
<dt class="col-sm-2">Enlace Gestiona</dt>
|
||||||
|
<dd class="col-sm-10">
|
||||||
|
<a href="@exp.FileUrl" target="_blank">@exp.FileUrl</a>
|
||||||
|
</dd>
|
||||||
|
}
|
||||||
|
</dl>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
// ============================================================
|
||||||
|
// CONFIG (mismo origen que GestionaService)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
private string ApiBase => Configuration["Gestiona:ApiBase"] ?? "";
|
||||||
|
private string AccessToken => Configuration["Gestiona:AccessToken"] ?? "";
|
||||||
|
|
||||||
|
private string RestBaseUrl =>
|
||||||
|
string.IsNullOrWhiteSpace(ApiBase)
|
||||||
|
? ""
|
||||||
|
: $"{ApiBase.TrimEnd('/')}/rest";
|
||||||
|
|
||||||
|
private readonly JsonSerializerOptions jsonOpts = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ESTADO UI
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
private string nifBuscado = string.Empty;
|
||||||
|
private string nifMostrado = string.Empty;
|
||||||
|
|
||||||
|
private const string ModoTodas = "todas";
|
||||||
|
private const string ModoRango = "rango";
|
||||||
|
private const string ModoUltimos = "ultimos";
|
||||||
|
|
||||||
|
private string modoFecha = ModoTodas;
|
||||||
|
|
||||||
|
private DateTime? fechaDesde;
|
||||||
|
private DateTime? fechaHasta;
|
||||||
|
|
||||||
|
private int mesesUltimos = 3;
|
||||||
|
|
||||||
|
private bool isSearching;
|
||||||
|
private bool haBuscado;
|
||||||
|
private string errorMessage = string.Empty;
|
||||||
|
|
||||||
|
private List<ExpedienteTerceroDto> expedientes = new();
|
||||||
|
|
||||||
|
// expediente cuyo detalle está abierto
|
||||||
|
private ExpedienteTerceroDto? expedienteSeleccionado;
|
||||||
|
|
||||||
|
private bool IsModo(string valor) => string.Equals(modoFecha, valor, StringComparison.Ordinal);
|
||||||
|
private void SetModo(string valor) => modoFecha = valor;
|
||||||
|
|
||||||
|
private void ToggleDetalle(ExpedienteTerceroDto exp)
|
||||||
|
{
|
||||||
|
if (expedienteSeleccionado == exp)
|
||||||
|
expedienteSeleccionado = null;
|
||||||
|
else
|
||||||
|
expedienteSeleccionado = exp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// MODELOS AUXILIARES (similares a tu servicio)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
private class LinkDto
|
||||||
|
{
|
||||||
|
public string Rel { get; set; } = string.Empty;
|
||||||
|
public string Href { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ThirdDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public string Nif { get; set; } = string.Empty;
|
||||||
|
public List<LinkDto> Links { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// BÚSQUEDA PRINCIPAL
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
private async Task BuscarAsync()
|
||||||
|
{
|
||||||
|
errorMessage = string.Empty;
|
||||||
|
haBuscado = false;
|
||||||
|
expedienteSeleccionado = null;
|
||||||
|
expedientes.Clear();
|
||||||
|
|
||||||
|
var nif = (nifBuscado ?? string.Empty).Trim().ToUpperInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(nif))
|
||||||
|
{
|
||||||
|
errorMessage = "Introduce un NIF para buscar.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(RestBaseUrl) || string.IsNullOrWhiteSpace(AccessToken))
|
||||||
|
{
|
||||||
|
errorMessage = "No está configurada la conexión a Gestiona (ApiBase / AccessToken).";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset? desde = null;
|
||||||
|
DateTimeOffset? hasta = null;
|
||||||
|
|
||||||
|
// Calcular rango según el modo
|
||||||
|
if (modoFecha == ModoRango)
|
||||||
|
{
|
||||||
|
if (!fechaDesde.HasValue || !fechaHasta.HasValue)
|
||||||
|
{
|
||||||
|
errorMessage = "Debes indicar las dos fechas (Desde y Hasta).";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
desde = new DateTimeOffset(fechaDesde.Value.Date, TimeSpan.Zero);
|
||||||
|
hasta = new DateTimeOffset(fechaHasta.Value.Date.AddDays(1).AddTicks(-1), TimeSpan.Zero);
|
||||||
|
|
||||||
|
if (desde > hasta)
|
||||||
|
{
|
||||||
|
errorMessage = "La fecha 'Desde' no puede ser mayor que 'Hasta'.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (modoFecha == ModoUltimos)
|
||||||
|
{
|
||||||
|
if (mesesUltimos <= 0)
|
||||||
|
{
|
||||||
|
errorMessage = "El número de meses debe ser mayor que 0.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ahora = DateTimeOffset.UtcNow;
|
||||||
|
desde = ahora.AddMonths(-mesesUltimos);
|
||||||
|
hasta = ahora;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isSearching = true;
|
||||||
|
|
||||||
|
// 1) Buscar el tercero por NIF
|
||||||
|
var tercero = await BuscarTerceroPorNifAsync(nif);
|
||||||
|
if (string.IsNullOrEmpty(tercero.Id) || string.IsNullOrEmpty(tercero.SelfHref))
|
||||||
|
{
|
||||||
|
errorMessage = "No se ha encontrado ningún tercero con ese NIF.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Listar expedientes usando third_rest_link
|
||||||
|
expedientes = await ObtenerExpedientesPorTerceroFiltradoAsync(
|
||||||
|
tercero.SelfHref,
|
||||||
|
desde,
|
||||||
|
hasta
|
||||||
|
);
|
||||||
|
|
||||||
|
nifMostrado = nif;
|
||||||
|
haBuscado = true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
errorMessage = $"Error al buscar expedientes: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isSearching = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// LLAMADAS A GESTIONA
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
private void AddBasicHeaders(HttpRequestMessage req)
|
||||||
|
{
|
||||||
|
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", AccessToken);
|
||||||
|
req.Headers.TryAddWithoutValidation("Accept", "application/vnd.gestiona.files-page+json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(string Id, string SelfHref)> BuscarTerceroPorNifAsync(string nif)
|
||||||
|
{
|
||||||
|
var filtro = new
|
||||||
|
{
|
||||||
|
result = new { max_results = 25 },
|
||||||
|
filter = new { nif }
|
||||||
|
};
|
||||||
|
var jsonFiltro = JsonSerializer.Serialize(filtro);
|
||||||
|
var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(jsonFiltro));
|
||||||
|
var url = $"{ApiBase.TrimEnd('/')}/rest/thirds?filter-view={Uri.EscapeDataString(b64)}";
|
||||||
|
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
|
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", AccessToken);
|
||||||
|
req.Headers.TryAddWithoutValidation("Accept", "application/vnd.gestiona.thirds-page+json");
|
||||||
|
|
||||||
|
using var resp = await Http.SendAsync(req);
|
||||||
|
resp.EnsureSuccessStatusCode();
|
||||||
|
var body = await resp.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(body);
|
||||||
|
if (!doc.RootElement.TryGetProperty("content", out var content) ||
|
||||||
|
content.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = content.EnumerateArray().FirstOrDefault(e =>
|
||||||
|
e.TryGetProperty("nif", out var nifProp) &&
|
||||||
|
string.Equals(nifProp.GetString(), nif, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (match.ValueKind == JsonValueKind.Undefined)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
var id = match.GetProperty("id").GetString() ?? string.Empty;
|
||||||
|
|
||||||
|
string selfHref = string.Empty;
|
||||||
|
if (match.TryGetProperty("links", out var linksEl) && linksEl.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
var selfLink = linksEl.EnumerateArray().FirstOrDefault(l =>
|
||||||
|
l.TryGetProperty("rel", out var rel) &&
|
||||||
|
string.Equals(rel.GetString(), "self", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (selfLink.ValueKind != JsonValueKind.Undefined &&
|
||||||
|
selfLink.TryGetProperty("href", out var hrefEl))
|
||||||
|
{
|
||||||
|
selfHref = hrefEl.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (id, selfHref);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ExpedienteTerceroDto>> ObtenerExpedientesPorTerceroFiltradoAsync(
|
||||||
|
string thirdSelfHref,
|
||||||
|
DateTimeOffset? desde,
|
||||||
|
DateTimeOffset? hasta)
|
||||||
|
{
|
||||||
|
var url = $"{ApiBase.TrimEnd('/')}/rest/files";
|
||||||
|
|
||||||
|
var bodyObj = new
|
||||||
|
{
|
||||||
|
third_rest_link = new
|
||||||
|
{
|
||||||
|
rel = "third",
|
||||||
|
href = thirdSelfHref
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(bodyObj, jsonOpts);
|
||||||
|
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Get, url)
|
||||||
|
{
|
||||||
|
Content = new StringContent(json, Encoding.UTF8, "application/vnd.gestiona.filter.files")
|
||||||
|
};
|
||||||
|
|
||||||
|
AddBasicHeaders(req);
|
||||||
|
|
||||||
|
using var resp = await Http.SendAsync(req);
|
||||||
|
var respBody = await resp.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"GET /rest/files filtrado: {(int)resp.StatusCode} {resp.ReasonPhrase}\n{respBody}");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var doc = JsonDocument.Parse(respBody);
|
||||||
|
|
||||||
|
JsonElement content;
|
||||||
|
if (doc.RootElement.TryGetProperty("content", out var contentEl) &&
|
||||||
|
contentEl.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
content = contentEl;
|
||||||
|
}
|
||||||
|
else if (doc.RootElement.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
content = doc.RootElement;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new List<ExpedienteTerceroDto>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var lista = new List<ExpedienteTerceroDto>();
|
||||||
|
|
||||||
|
foreach (var item in content.EnumerateArray())
|
||||||
|
{
|
||||||
|
// ===== Fecha creación (string o número) =====
|
||||||
|
DateTimeOffset? creation = null;
|
||||||
|
if (item.TryGetProperty("creation_date", out var pCreation))
|
||||||
|
{
|
||||||
|
if (pCreation.ValueKind == JsonValueKind.Number &&
|
||||||
|
pCreation.TryGetInt64(out var tsNum))
|
||||||
|
{
|
||||||
|
creation = DateTimeOffset.FromUnixTimeSeconds(tsNum);
|
||||||
|
}
|
||||||
|
else if (pCreation.ValueKind == JsonValueKind.String &&
|
||||||
|
long.TryParse(pCreation.GetString(), out var tsStr))
|
||||||
|
{
|
||||||
|
creation = DateTimeOffset.FromUnixTimeSeconds(tsStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (desde.HasValue && creation.HasValue && creation.Value < desde.Value)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (hasta.HasValue && creation.HasValue && creation.Value > hasta.Value)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// ===== URLs =====
|
||||||
|
string? selfHref = null;
|
||||||
|
if (item.TryGetProperty("links", out var linksEl) &&
|
||||||
|
linksEl.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var l in linksEl.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (l.TryGetProperty("rel", out var relProp) &&
|
||||||
|
string.Equals(relProp.GetString(), "self", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
l.TryGetProperty("href", out var hrefProp))
|
||||||
|
{
|
||||||
|
selfHref = hrefProp.GetString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string? ehomeUrl = null;
|
||||||
|
if (item.TryGetProperty("ehome_url", out var pEhome) &&
|
||||||
|
pEhome.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
ehomeUrl = pEhome.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileUrl = ehomeUrl ?? selfHref;
|
||||||
|
if (string.IsNullOrWhiteSpace(fileUrl))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var code = GetJsonString(item, "code");
|
||||||
|
var subject = GetJsonString(item, "subject");
|
||||||
|
var freeTitle = GetJsonString(item, "free_title");
|
||||||
|
var selectableTitle = GetJsonString(item, "selectable_title");
|
||||||
|
var procedureName = GetJsonString(item, "procedure_name");
|
||||||
|
var asunto = FirstNonEmpty(freeTitle, subject, selectableTitle, procedureName) ?? string.Empty;
|
||||||
|
|
||||||
|
// ===== Estado (state / status / full_state) =====
|
||||||
|
string? state = null;
|
||||||
|
if (item.TryGetProperty("state", out var pState) && pState.ValueKind == JsonValueKind.String)
|
||||||
|
state = pState.GetString();
|
||||||
|
else if (item.TryGetProperty("status", out var pStatus) && pStatus.ValueKind == JsonValueKind.String)
|
||||||
|
state = pStatus.GetString();
|
||||||
|
else if (item.TryGetProperty("full_state", out var pFullState) && pFullState.ValueKind == JsonValueKind.String)
|
||||||
|
state = pFullState.GetString();
|
||||||
|
|
||||||
|
var dto = new ExpedienteTerceroDto
|
||||||
|
{
|
||||||
|
FileUrl = fileUrl!,
|
||||||
|
CodigoExpediente = code ?? string.Empty,
|
||||||
|
Asunto = asunto,
|
||||||
|
FechaCreacion = creation,
|
||||||
|
Estado = state ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
lista.Add(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lista;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetJsonString(JsonElement item, string propertyName)
|
||||||
|
{
|
||||||
|
return item.TryGetProperty(propertyName, out var property) &&
|
||||||
|
property.ValueKind == JsonValueKind.String
|
||||||
|
? property.GetString()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FirstNonEmpty(params string?[] values)
|
||||||
|
{
|
||||||
|
foreach (var value in values)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,14 @@
|
|||||||
@page "/Gestiona"
|
@page "/Gestiona"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
@attribute [Authorize]
|
||||||
@using GestionaDenunciasAN.Models
|
@using GestionaDenunciasAN.Models
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@attribute [StreamRendering]
|
@attribute [StreamRendering]
|
||||||
@inject GestionaDenunciasAN.Models.UserState userState
|
@inject GestionaDenunciasAN.Models.UserState userState
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IHostEnvironment HostEnvironment
|
@inject IHostEnvironment HostEnvironment
|
||||||
|
@inject IDenunciaStore DenunciaStore
|
||||||
|
@inject IGestionaService GestionaApi
|
||||||
|
|
||||||
<PageTitle>Denuncias Gestión</PageTitle>
|
<PageTitle>Denuncias Gestión</PageTitle>
|
||||||
|
|
||||||
@@ -107,8 +110,7 @@ else
|
|||||||
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia</h5>
|
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia</h5>
|
||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<span><strong>Estado:</strong> @denuncia.Estado</span>
|
<span><strong>Estado:</strong> @denuncia.Estado</span>
|
||||||
<span><strong>Nombre:</strong> @denuncia.NombreDenuncia</span>
|
<span><strong>Asunto:</strong> @denuncia.NombreDenuncia</span>
|
||||||
<span><strong>Archivo:</strong> @denuncia.ArchivoElegido</span>
|
|
||||||
<span><strong>Fecha de Subida:</strong> @denuncia.FechaSubidaAGestiona.ToString("dd/MM/yyyy")</span>
|
<span><strong>Fecha de Subida:</strong> @denuncia.FechaSubidaAGestiona.ToString("dd/MM/yyyy")</span>
|
||||||
<span><strong>Hora de Subida:</strong> @denuncia.FechaSubidaAGestiona.ToString("HH:mm")</span>
|
<span><strong>Hora de Subida:</strong> @denuncia.FechaSubidaAGestiona.ToString("HH:mm")</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,10 +135,10 @@ else
|
|||||||
<dt class="col-sm-3">Fecha</dt>
|
<dt class="col-sm-3">Fecha</dt>
|
||||||
<dd class="col-sm-9">@denuncia.Fecha.ToString("dd/MM/yyyy HH:mm")</dd>
|
<dd class="col-sm-9">@denuncia.Fecha.ToString("dd/MM/yyyy HH:mm")</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Expediente_Gestiona))
|
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
||||||
{
|
{
|
||||||
<dt class="col-sm-3">Expediente Gestión</dt>
|
<dt class="col-sm-3">Nº expediente Gestiona</dt>
|
||||||
<dd class="col-sm-9">@denuncia.Expediente_Gestiona</dd>
|
<dd class="col-sm-9">@denuncia.ExpedienteGestionaMostrable</dd>
|
||||||
}
|
}
|
||||||
@if (denuncia.Id_Persona_Gestiona != 0)
|
@if (denuncia.Id_Persona_Gestiona != 0)
|
||||||
{
|
{
|
||||||
@@ -171,6 +173,7 @@ else
|
|||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Sexo))
|
@if (!string.IsNullOrWhiteSpace(denuncia.Sexo))
|
||||||
{
|
{
|
||||||
<dt class="col-sm-3">Sexo</dt>
|
<dt class="col-sm-3">Sexo</dt>
|
||||||
|
|
||||||
<dd class="col-sm-9">@denuncia.Sexo</dd>
|
<dd class="col-sm-9">@denuncia.Sexo</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Dni))
|
@if (!string.IsNullOrWhiteSpace(denuncia.Dni))
|
||||||
@@ -308,8 +311,6 @@ else
|
|||||||
// Variable para la búsqueda
|
// Variable para la búsqueda
|
||||||
private string busqueda = "";
|
private string busqueda = "";
|
||||||
|
|
||||||
private const string DENUNCIAS_JSON = @"C:\ZipsDenuncias\denuncias.json";
|
|
||||||
private const string FICHEROS_JSON = @"C:\ZipsDenuncias\ficheros.json";
|
|
||||||
private bool isLoading = false;
|
private bool isLoading = false;
|
||||||
private bool hasLoaded = false;
|
private bool hasLoaded = false;
|
||||||
|
|
||||||
@@ -317,11 +318,6 @@ else
|
|||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(userState.Token))
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
await CargarGestionaAsync();
|
await CargarGestionaAsync();
|
||||||
@@ -335,29 +331,22 @@ else
|
|||||||
private async Task CargarGestionaAsync()
|
private async Task CargarGestionaAsync()
|
||||||
{
|
{
|
||||||
var todas = await CargarDenunciasJsonAsync();
|
var todas = await CargarDenunciasJsonAsync();
|
||||||
denunciasGestiona = todas.Where(d => d.Expediente_Gestiona == "Gestiona").ToList();
|
denunciasGestiona = todas
|
||||||
|
.Where(d => d.EnGestiona)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await SincronizarExpedientesGestionaAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<List<DenunciasGestiona>> CargarDenunciasJsonAsync()
|
private async Task<List<DenunciasGestiona>> CargarDenunciasJsonAsync()
|
||||||
{
|
{
|
||||||
if (File.Exists(DENUNCIAS_JSON))
|
return await DenunciaStore.GetAllDenunciasAsync();
|
||||||
{
|
|
||||||
var json = await File.ReadAllTextAsync(DENUNCIAS_JSON);
|
|
||||||
var lista = Newtonsoft.Json.JsonConvert.DeserializeObject<List<DenunciasGestiona>>(json);
|
|
||||||
return lista ?? new List<DenunciasGestiona>();
|
|
||||||
}
|
|
||||||
return new List<DenunciasGestiona>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<FicherosDenuncias>> CargarFicherosJsonAsync()
|
private async Task<List<FicherosDenuncias>> CargarFicherosJsonAsync()
|
||||||
{
|
{
|
||||||
if (File.Exists(FICHEROS_JSON))
|
return await DenunciaStore.GetAllFicherosAsync();
|
||||||
{
|
|
||||||
var json = await File.ReadAllTextAsync(FICHEROS_JSON);
|
|
||||||
var lista = Newtonsoft.Json.JsonConvert.DeserializeObject<List<FicherosDenuncias>>(json);
|
|
||||||
return lista ?? new List<FicherosDenuncias>();
|
|
||||||
}
|
|
||||||
return new List<FicherosDenuncias>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CargarFicherosAdjuntosAsync()
|
private async Task CargarFicherosAdjuntosAsync()
|
||||||
@@ -380,4 +369,49 @@ else
|
|||||||
_ => "application/octet-stream",
|
_ => "application/octet-stream",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SincronizarExpedientesGestionaAsync()
|
||||||
|
{
|
||||||
|
foreach (var denuncia in denunciasGestiona.Where(d =>
|
||||||
|
string.IsNullOrWhiteSpace(d.CodigoExpedienteGestiona) &&
|
||||||
|
!string.IsNullOrWhiteSpace(d.Expediente_Gestiona) &&
|
||||||
|
!string.Equals(d.Expediente_Gestiona, "Pendiente", StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var expediente = await GestionaApi.ObtenerExpedienteAsync(denuncia.Expediente_Gestiona);
|
||||||
|
if (expediente is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
|
denuncia.Expediente_Gestiona = expediente.FileUrl;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(expediente.CodigoExpediente) &&
|
||||||
|
!string.Equals(denuncia.CodigoExpedienteGestiona, expediente.CodigoExpediente, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
denuncia.CodigoExpedienteGestiona = expediente.CodigoExpediente;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(expediente.FreeTitle) &&
|
||||||
|
!string.Equals(denuncia.NombreDenuncia, expediente.FreeTitle, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
denuncia.NombreDenuncia = expediente.FreeTitle;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
{
|
||||||
|
await DenunciaStore.UpsertDenunciaAsync(denuncia);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// No bloqueamos la pantalla si Gestiona no devuelve metadatos.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/Instrucciones"
|
@page "/Instrucciones"
|
||||||
|
@attribute [Authorize]
|
||||||
@attribute [StreamRendering]
|
@attribute [StreamRendering]
|
||||||
@inject GestionaDenunciasAN.Models.UserState userState
|
@inject GestionaDenunciasAN.Models.UserState userState
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@@ -6,70 +7,116 @@
|
|||||||
<PageTitle>Instrucciones</PageTitle>
|
<PageTitle>Instrucciones</PageTitle>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
<h1 class="mb-4">Instrucciones de la Aplicación</h1>
|
<h1 class="mb-4">Guía de Uso — Gestión de Denuncias</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Bienvenido a la aplicación de <strong>Gestión de Denuncias</strong>. Esta herramienta ha sido diseñada para
|
Esta aplicación permite procesar denuncias desde archivos ZIP y gestionarlas en tres etapas:
|
||||||
cargar, procesar y gestionar denuncias que se reciben a través de archivos ZIP. A continuación, se explica el funcionamiento y las principales funcionalidades:
|
<strong>Pendientes</strong>, <strong>Gestión</strong> (aceptadas) y <strong>Rechazadas</strong>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2>1. Carga y Procesamiento de Denuncias</h2>
|
<h2>1. Carga de ZIPs</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<strong>Carga de archivos:</strong> Los archivos ZIP se encuentran en la carpeta <code>Ejemplos</code>. Cada ZIP contiene un archivo <code>report.txt</code> con los datos de la denuncia, y en algunos casos, ficheros asociados.
|
Sitúate en la pestaña <strong>Gestión de ZIP</strong>. Haz clic en <em>Subir nuevo ZIP</em>,
|
||||||
|
selecciona uno o varios archivos <code>.zip</code> y espera a que se extraigan.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Descompresión y parseo:</strong> La aplicación descomprime cada ZIP y extrae la información del <code>report.txt</code> para crear una representación estructurada de la denuncia.
|
Cada ZIP debe incluir un <code>report.txt</code> con los campos de la denuncia, y opcionalmente
|
||||||
|
subcarpetas <code>files</code> o <code>files_attached_from_recipients</code> con PDF e imágenes.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Tras el procesado, la app lee los <code>report.txt</code> y actualiza la base de datos:
|
||||||
|
- El listado de <strong>Pendientes</strong>.
|
||||||
|
- El registro de denuncias.
|
||||||
|
- El registro de ficheros adjuntos.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>2. Visualización y Gestión de Denuncias</h2>
|
<h2>2. Pestaña <strong>Pendientes</strong></h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<strong>Pendientes:</strong> En esta pestaña se listan todas las denuncias recién cargadas. Cada denuncia se muestra en una tarjeta colapsable que permite ver sus detalles y los ficheros asociados. Además, se disponen de botones para <em>Enviar a gestiona</em> (verde) y <em>Rechazar denuncia</em> (rojo). Actualmente, estos botones no realizan ninguna acción, pero en futuras versiones registrarán el estado de la denuncia.
|
Verás cada denuncia en una tarjeta colapsable con sus datos y el listado de ficheros adjuntos.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Finalizados:</strong> Una vez procesadas, las denuncias se trasladan a la pestaña <strong>Finalizados</strong>, donde se muestran diferenciadas:
|
Hay dos acciones:
|
||||||
<ul>
|
<ul>
|
||||||
<li>Las denuncias aceptadas se destacan con un fondo verde.</li>
|
|
||||||
<li>Las denuncias rechazadas se muestran con un fondo rojo.</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<strong>Visualización de ficheros:</strong> En cada tarjeta, el botón <em>Ver</em> abre una nueva pestaña para mostrar el contenido del fichero asociado.
|
<strong>Configurar subida</strong> (verde): abre un modal donde puedes:
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>3. Flujo de Trabajo</h2>
|
|
||||||
<p>
|
|
||||||
El flujo de trabajo de la aplicación es el siguiente:
|
|
||||||
</p>
|
|
||||||
<ol>
|
<ol>
|
||||||
|
<li>Poner un nombre descriptivo.</li>
|
||||||
<li>
|
<li>
|
||||||
Los archivos ZIP se depositan en la carpeta <code>Ejemplos</code>.
|
Elegir el modo de subida:
|
||||||
|
<ul>
|
||||||
|
<li><em>Unir</em> todos los ficheros en un único PDF.</li>
|
||||||
|
<li><em>Subir</em> cada fichero de forma independiente.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>Seleccionar el grupo de destino (600, 510 o 700).</li>
|
||||||
|
<li>
|
||||||
|
Confirmar. La denuncia se crea y abre en Gestióna, sube los documentos
|
||||||
|
y pasa a la pestaña <strong>Gestión</strong>.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
La aplicación descomprime los ZIP y parsea el archivo <code>report.txt</code> para extraer la información de cada denuncia.
|
<strong>Rechazar denuncia</strong> (rojo): abre un modal para poner el motivo.
|
||||||
|
Al confirmar, la denuncia se marca como rechazada y va a la pestaña
|
||||||
|
<strong>Rechazados</strong>.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>3. Pestaña <strong>Gestión</strong></h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Aquí se listan las denuncias que ya han sido <em>enviadas a Gestión</em>.
|
||||||
|
Aparecen con fondo verde.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Las denuncias se muestran en la pestaña <strong>Pendientes</strong> en forma de tarjetas colapsables, donde puedes ver detalles y ficheros asociados.
|
Cada tarjeta muestra:
|
||||||
|
<ul>
|
||||||
|
<li>ID, nombre, archivo subido</li>
|
||||||
|
<li>Fecha y hora de subida</li>
|
||||||
|
<li>Detalles completos y enlaces “Ver” a los PDFs/imágenes</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>4. Pestaña <strong>Rechazadas</strong></h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Aquí verás todas las denuncias que han sido rechazadas. Fondo rojo.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Las acciones para enviar o rechazar la denuncia se registrarán y, en consecuencia, se reflejará su estado en la pestaña <strong>Finalizados</strong>.
|
Cada tarjeta muestra el motivo de rechazo y la fecha/hora en que se marcó.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>5. Flujo completo</h2>
|
||||||
|
<ol>
|
||||||
|
<li>Subes uno o varios ZIP en la pestaña <strong>Gestión de ZIP</strong>.</li>
|
||||||
|
<li>La aplicación extrae y parsea informes, los añade a <strong>Pendientes</strong>.</li>
|
||||||
|
<li>
|
||||||
|
En <strong>Pendientes</strong> eliges qué hacer con cada denuncia:
|
||||||
|
<ul>
|
||||||
|
<li><strong>Configurar subida</strong> → pasa a <strong>Gestión</strong>.</li>
|
||||||
|
<li><strong>Rechazar denuncia</strong> → pasa a <strong>Rechazadas</strong>.</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
En <strong>Gestión</strong> puedes revisar lo ya subido; en
|
||||||
|
<strong>Rechazadas</strong> ves los motivos.
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<p>
|
<p class="mt-4">
|
||||||
Esta aplicación está diseñada para facilitar la gestión de denuncias de forma intuitiva. Si tienes cualquier duda o sugerencia, no dudes en ponerte en contacto con el equipo de soporte.
|
Con este flujo tienes control total sobre:
|
||||||
|
<strong>nombre</strong>, <strong>modo de subida</strong>, <strong>grupo destino</strong> y
|
||||||
|
<strong>estado final</strong> de cada denuncia.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
// Si no hay token, redirige al login
|
|
||||||
if (string.IsNullOrEmpty(userState.Token))
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,344 +1,253 @@
|
|||||||
@using BlazorBootstrap
|
|
||||||
@using Layout
|
|
||||||
@using Microsoft.AspNetCore.Mvc
|
|
||||||
@using Newtonsoft.Json
|
|
||||||
@using Newtonsoft.Json.Linq
|
|
||||||
@using GestionaDenunciasAN.Models
|
|
||||||
@using System.Net.Http.Headers
|
|
||||||
@using System.Text
|
|
||||||
@using System.Linq.Expressions
|
|
||||||
@using Serialize.Linq.Serializers
|
|
||||||
@using System.Security.Cryptography.X509Certificates
|
|
||||||
@using System.Security.Cryptography
|
|
||||||
@using bdAntifraude.db
|
|
||||||
|
|
||||||
@rendermode InteractiveServer
|
|
||||||
@layout EmptyLayout
|
|
||||||
@page "/"
|
@page "/"
|
||||||
@inject IHttpClientFactory HttpClientFactory
|
@layout EmptyLayout
|
||||||
@inject IHttpContextAccessor HttpContextAccessor
|
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
||||||
@inject NavigationManager Navigation
|
@using System.Text.Json
|
||||||
@inject UserState UserState
|
@using GestionaDenunciasAN.Models
|
||||||
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
|
||||||
<PageTitle>Gestión Denuncias - Oficina Antifraude de Andalucía</PageTitle>
|
<PageTitle>Portal de denuncias</PageTitle>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Contenedor que ocupa toda la altura de la ventana */
|
.login-shell {
|
||||||
.full-height {
|
min-height: 100vh;
|
||||||
height: 100vh;
|
background:
|
||||||
margin: 0;
|
radial-gradient(circle at top left, rgba(90, 155, 213, 0.3), transparent 35%),
|
||||||
padding: 0;
|
linear-gradient(135deg, #f7fafc 0%, #dfe8f3 45%, #eef4fb 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Columna izquierda con un degradado en azul */
|
.login-panel {
|
||||||
.left-side {
|
max-width: 440px;
|
||||||
background: linear-gradient(135deg, #5a9bd5, #2A5298);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Columna derecha con fondo claro */
|
|
||||||
.right-side {
|
|
||||||
background: linear-gradient(135deg, #ffffff, #dddddd);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Contenedor para centrar contenido en ambas columnas */
|
|
||||||
.centered-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
padding: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Logo */
|
|
||||||
.logo-img {
|
|
||||||
max-width: 300px;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Caja de login con degradado y sombra */
|
|
||||||
.login-box {
|
|
||||||
background: linear-gradient(135deg, #5a9bd5, #2A5298);
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
||||||
max-width: 400px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
background: rgba(255, 255, 255, 0.92);
|
||||||
color: #fff; /* Para que el texto sea legible sobre el gradiente */
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 25px 60px rgba(31, 73, 125, 0.18);
|
||||||
|
border: 1px solid rgba(90, 155, 213, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-box label {
|
.brand-panel {
|
||||||
font-weight: 500;
|
color: #12395f;
|
||||||
display: block;
|
max-width: 560px;
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 0.3rem;
|
|
||||||
color: #fff; /* Aseguramos que la etiqueta se vea bien */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-box .form-control {
|
.brand-kicker {
|
||||||
width: 100%;
|
display: inline-block;
|
||||||
padding: 0.75rem;
|
padding: 0.35rem 0.75rem;
|
||||||
margin-bottom: 1rem;
|
border-radius: 999px;
|
||||||
border: 1px solid #ccc;
|
background: rgba(42, 82, 152, 0.12);
|
||||||
border-radius: 5px;
|
color: #1f497d;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-message {
|
.brand-title {
|
||||||
color: red;
|
font-size: clamp(2rem, 4vw, 3.5rem);
|
||||||
margin-bottom: 1rem;
|
line-height: 1.05;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Botones personalizados */
|
.brand-copy {
|
||||||
.login-box .btnOAAFAzul {
|
font-size: 1.05rem;
|
||||||
background-color: #fff !important; /* Fondo blanco */
|
color: #35597f;
|
||||||
color: #000 !important; /* Texto negro */
|
max-width: 34rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-label {
|
||||||
|
font-size: 0.92rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #244e79;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input {
|
||||||
|
border-radius: 14px;
|
||||||
|
min-height: 3rem;
|
||||||
|
border-color: #c7d6e7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input:focus {
|
||||||
|
border-color: #2a5298;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(42, 82, 152, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-button {
|
||||||
|
min-height: 3rem;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(135deg, #2a5298, #1f497d);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 15px;
|
font-weight: 600;
|
||||||
padding: 10px;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-box .btnOAAFAzul:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.btnOAAFBlack {
|
|
||||||
background-color: #343334;
|
|
||||||
border: none;
|
|
||||||
border-radius: 15px;
|
|
||||||
color: white;
|
|
||||||
padding: 10px;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btnOAAFBlack:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el loading */
|
|
||||||
.loadingFrame {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingDiv {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loadingImg {
|
|
||||||
background: url('Content/imagenes/loading.gif') no-repeat center center;
|
|
||||||
background-size: contain;
|
|
||||||
width: 100px;
|
|
||||||
height: 100px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<link rel="icon" type="image/x-icon" href="faviconParlamento.ico" />
|
<div class="container-fluid login-shell d-flex align-items-center">
|
||||||
<link href="~/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
<div class="container py-5">
|
||||||
<link href="Content/Site.css" rel="stylesheet" />
|
<div class="row g-5 align-items-center">
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="brand-panel">
|
||||||
|
<span class="brand-kicker">Oficina Antifraude de Andalucía</span>
|
||||||
|
<h1 class="brand-title">Una sola puerta para entrar, importar y tramitar denuncias.</h1>
|
||||||
|
<p class="brand-copy mb-4">
|
||||||
|
El acceso de la aplicación ya se apoya en GlobalLeaks. La sesión interna de esta app se mantiene
|
||||||
|
activa de forma persistente, y cuando caduque la sesión de obtención de denuncias solo habrá que
|
||||||
|
renovar el código 2FA desde la bandeja de entrada.
|
||||||
|
</p>
|
||||||
|
<img src="Content/imagenes/logo-oaaf-negativo-transparente.svg"
|
||||||
|
alt="Logo Oficina Antifraude"
|
||||||
|
style="max-width: 320px; width: 100%;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Bloque de loading que se muestra mientras 'mostrar' es true -->
|
<div class="col-lg-5 d-flex justify-content-center">
|
||||||
@if (mostrar)
|
<div class="login-panel p-4 p-md-5">
|
||||||
|
<h2 class="h3 mb-2">Acceso con GlobalLeaks</h2>
|
||||||
|
<p class="text-muted mb-4">
|
||||||
|
Introduce tu usuario, contraseña y el código 2FA actual para dejar la app iniciada.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(StatusMessage))
|
||||||
{
|
{
|
||||||
<div id="cargando" class="loadingFrame">
|
<div class="alert @StatusCss mb-4">@StatusMessage</div>
|
||||||
<div class="loadingDiv">
|
|
||||||
<div class="loadingImg"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Estructura en dos columnas -->
|
<div class="mb-3">
|
||||||
<div class="container-fluid full-height px-0">
|
<label class="login-label mb-2">Usuario</label>
|
||||||
<div class="row no-gutters full-height">
|
<input class="form-control login-input"
|
||||||
<!-- Columna izquierda (más ancha) -->
|
@bind="Username"
|
||||||
<div class="col-md-7 left-side">
|
autocomplete="username"
|
||||||
<div class="centered-content">
|
placeholder="usuario de GlobalLeaks" />
|
||||||
<!-- Tu logo en grande -->
|
|
||||||
<img src="Content/imagenes/logo-oaaf-negativo-transparente.svg" alt="Logo Oficina Antifraude" class="logo-img" />
|
|
||||||
<h1>Oficina Antifraude de Andalucía</h1>
|
|
||||||
<p style="max-width: 500px; text-align: center;">
|
|
||||||
Bienvenido/a a la plataforma de gestión de denuncias.
|
|
||||||
Aquí podrás autenticarte para revisar y tramitar las denuncias recibidas.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Columna derecha (más estrecha) -->
|
<div class="mb-3">
|
||||||
<div class="col-md-5 right-side">
|
<label class="login-label mb-2">Contraseña</label>
|
||||||
<div class="centered-content">
|
<input class="form-control login-input"
|
||||||
<form class="login-box">
|
type="password"
|
||||||
<h3 class="mb-4">Iniciar Sesión</h3>
|
@bind="Password"
|
||||||
|
autocomplete="current-password" />
|
||||||
<p id="mensajeError" class="error-message">@mensaje</p>
|
|
||||||
|
|
||||||
<div class="form-group text-left">
|
|
||||||
<label for="Usu">Usuario</label>
|
|
||||||
<input id="Usu" type="text" class="form-control" @bind="Usu" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group text-left">
|
<div class="mb-4">
|
||||||
<label for="Contrasena">Contraseña</label>
|
<label class="login-label mb-2">Código 2FA</label>
|
||||||
<input id="Contrasena" type="password" class="form-control" @bind="pass" />
|
<input class="form-control login-input"
|
||||||
|
@bind="Authcode"
|
||||||
|
@onkeydown="HandleAuthcodeKeyDown"
|
||||||
|
inputmode="numeric"
|
||||||
|
maxlength="6"
|
||||||
|
placeholder="123456" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btnOAAFAzul" type="button" @onclick="LogIn">ENTRAR</button>
|
<button class="btn btn-primary login-button w-100"
|
||||||
<button class="btnOAAFBlack" type="button" @onclick="IniciarSesionConCertificado">INICIAR SESIÓN CON CERTIFICADO</button>
|
@onclick="LoginAsync"
|
||||||
</form>
|
disabled="@IsBusy">
|
||||||
|
@(IsBusy ? "Conectando..." : "Entrar")
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Iframe oculto para la autenticación con certificado -->
|
|
||||||
<iframe id="authCertIframe" style="display:none;"></iframe>
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
public string? Usu { get; set; }
|
[SupplyParameterFromQuery(Name = "returnUrl")]
|
||||||
public string? pass { get; set; }
|
public string? ReturnUrl { get; set; }
|
||||||
private string? mensaje { get; set; }
|
|
||||||
public bool mostrar { get; set; } = false;
|
|
||||||
private DotNetObjectReference<Login>? dotNetRef;
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
private string Username { get; set; } = string.Empty;
|
||||||
|
private string Password { get; set; } = string.Empty;
|
||||||
|
private string Authcode { get; set; } = string.Empty;
|
||||||
|
private string StatusMessage { get; set; } = string.Empty;
|
||||||
|
private string StatusCss { get; set; } = "alert-info";
|
||||||
|
private bool IsBusy { get; set; }
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (firstRender)
|
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||||
|
if (authState.User.Identity?.IsAuthenticated == true)
|
||||||
{
|
{
|
||||||
// Se crea una referencia a este componente para que JS pueda invocar SetToken
|
Navigation.NavigateTo(GetTargetUrl(), true);
|
||||||
dotNetRef = DotNetObjectReference.Create(this);
|
|
||||||
await JSRuntime.InvokeVoidAsync("registerTokenReceiver", dotNetRef);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnInitialized()
|
private async Task LoginAsync()
|
||||||
{
|
{
|
||||||
LimpiarEstadoUsuario();
|
StatusMessage = string.Empty;
|
||||||
}
|
|
||||||
|
|
||||||
private void LimpiarEstadoUsuario()
|
if (string.IsNullOrWhiteSpace(Username) ||
|
||||||
|
string.IsNullOrWhiteSpace(Password) ||
|
||||||
|
string.IsNullOrWhiteSpace(Authcode))
|
||||||
{
|
{
|
||||||
UserState.Token = "";
|
SetStatus("Debes rellenar usuario, contraseña y código 2FA.", "alert-warning");
|
||||||
UserState.NombreUsu = "";
|
|
||||||
HttpContextAccessor?.HttpContext?.Session?.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task LogIn()
|
|
||||||
{
|
|
||||||
mostrar = true;
|
|
||||||
await Task.Delay(1);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(Usu) || string.IsNullOrWhiteSpace(pass))
|
|
||||||
{
|
|
||||||
mostrar = false;
|
|
||||||
mensaje = "Por favor, ingrese su usuario y contraseña.";
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IsBusy = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var client = HttpClientFactory.CreateClient();
|
var response = await JSRuntime.InvokeAsync<ApiJsResponse>(
|
||||||
client.BaseAddress = new Uri(Utilidades.urlSwagger());
|
"appAuthPostJson",
|
||||||
var loginPayload = new { NombreUsuario = Usu, Contraseña = pass, Origen = "Denuncias" };
|
"/api/auth/login",
|
||||||
var loginContent = new StringContent(JsonConvert.SerializeObject(loginPayload), Encoding.UTF8, "application/json");
|
new LoginRequest(Username.Trim(), Password, Authcode.Trim()));
|
||||||
var loginResponse = await client.PostAsync("Auth/login", loginContent);
|
|
||||||
await ProcesarRespuesta(loginResponse);
|
if (!response.Ok)
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
{
|
||||||
mostrar = false;
|
var error = ReadData<ApiError>(response);
|
||||||
mensaje = $"Error inesperado: {ex.Message}";
|
SetStatus(error?.Error ?? "No se ha podido iniciar sesión.", "alert-danger");
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task IniciarSesionConCertificado()
|
Navigation.NavigateTo(GetTargetUrl(), true);
|
||||||
{
|
|
||||||
mostrar = true;
|
|
||||||
var url = Utilidades.urlSwagger() + "Auth/login-cert?iframe=true";
|
|
||||||
await JSRuntime.InvokeVoidAsync("iniciarSesionConCertificado", url);
|
|
||||||
}
|
}
|
||||||
|
catch (JSException ex)
|
||||||
private async Task ProcesarRespuesta(HttpResponseMessage response)
|
|
||||||
{
|
{
|
||||||
var responseContent = await response.Content.ReadAsStringAsync();
|
SetStatus($"Fallo de comunicación con el navegador: {ex.Message}", "alert-danger");
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
var parsedJson = JObject.Parse(responseContent);
|
|
||||||
UserState.Token = parsedJson["token"]?.ToString() ?? "";
|
|
||||||
// Actualizamos el nombre del usuario (formateado como "APELLIDOS, NOMBRE")
|
|
||||||
UserState.NombreUsu = $"{parsedJson["user"]?["apellidos"]?.ToString()}, {parsedJson["user"]?["nombre"]?.ToString()}";
|
|
||||||
Navigation.NavigateTo("/GestionZip", true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mostrar = false;
|
|
||||||
mensaje = "Error de autenticación. Verifique sus credenciales o el certificado.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public Task SetToken(string token, string userJson)
|
|
||||||
{
|
|
||||||
// Actualizamos el token en UserState
|
|
||||||
UserState.Token = token;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var userObj = JObject.Parse(userJson);
|
|
||||||
UserState.NombreUsu = $"{userObj["APELLIDOS"]?.ToString()}, {userObj["NOMBRE"]?.ToString()}";
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
UserState.NombreUsu = "";
|
SetStatus("No se ha podido conectar con el servidor.", "alert-danger");
|
||||||
}
|
}
|
||||||
Navigation.NavigateTo("/GestionZip", true);
|
finally
|
||||||
return Task.CompletedTask;
|
{
|
||||||
|
IsBusy = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
<script>
|
private Task HandleAuthcodeKeyDown(KeyboardEventArgs args)
|
||||||
function registerTokenReceiver(dotnetRef) {
|
=> args.Key == "Enter" ? LoginAsync() : Task.CompletedTask;
|
||||||
window.dotnetTokenReceiver = dotnetRef;
|
|
||||||
|
private void SetStatus(string message, string cssClass)
|
||||||
|
{
|
||||||
|
StatusMessage = message;
|
||||||
|
StatusCss = cssClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.iniciarSesionConCertificado = function(url) {
|
private string GetTargetUrl()
|
||||||
console.log("Se llamó iniciarSesionConCertificado con URL:", url);
|
{
|
||||||
var iframe = document.getElementById("authCertIframe");
|
if (!string.IsNullOrWhiteSpace(ReturnUrl) &&
|
||||||
if (iframe) {
|
ReturnUrl.StartsWith("/", StringComparison.Ordinal) &&
|
||||||
iframe.src = url;
|
!ReturnUrl.StartsWith("//", StringComparison.Ordinal))
|
||||||
} else {
|
{
|
||||||
console.error("No se encontró el iframe con id 'authCertIframe'");
|
return ReturnUrl;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("message", function(event) {
|
return "/GestionZip";
|
||||||
var data = event.data;
|
}
|
||||||
if (data && data.token) {
|
|
||||||
console.log("Mensaje recibido con token:", data.token);
|
private static T? ReadData<T>(ApiJsResponse response)
|
||||||
if (window.dotnetTokenReceiver) {
|
{
|
||||||
window.dotnetTokenReceiver.invokeMethodAsync("SetToken", data.token, JSON.stringify(data.user));
|
if (response.Data.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
|
||||||
} else {
|
{
|
||||||
localStorage.setItem("token", data.token);
|
return default;
|
||||||
localStorage.setItem("user", JSON.stringify(data.user));
|
}
|
||||||
window.location.href = "/GestionZip";
|
|
||||||
|
return response.Data.Deserialize<T>(new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ApiJsResponse
|
||||||
|
{
|
||||||
|
public bool Ok { get; set; }
|
||||||
|
public int Status { get; set; }
|
||||||
|
public JsonElement Data { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,12 @@
|
|||||||
@page "/Rechazados"
|
@page "/Rechazados"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
@attribute [Authorize]
|
||||||
@using GestionaDenunciasAN.Models
|
@using GestionaDenunciasAN.Models
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@attribute [StreamRendering]
|
@attribute [StreamRendering]
|
||||||
@inject GestionaDenunciasAN.Models.UserState userState
|
@inject GestionaDenunciasAN.Models.UserState userState
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
@inject IDenunciaStore DenunciaStore
|
||||||
|
|
||||||
<PageTitle>Denuncias Rechazadas</PageTitle>
|
<PageTitle>Denuncias Rechazadas</PageTitle>
|
||||||
|
|
||||||
@@ -130,10 +132,10 @@ else
|
|||||||
<dt class="col-sm-3">Fecha</dt>
|
<dt class="col-sm-3">Fecha</dt>
|
||||||
<dd class="col-sm-9">@denuncia.Fecha.ToString("dd/MM/yyyy HH:mm")</dd>
|
<dd class="col-sm-9">@denuncia.Fecha.ToString("dd/MM/yyyy HH:mm")</dd>
|
||||||
}
|
}
|
||||||
@if (!string.IsNullOrWhiteSpace(denuncia.Expediente_Gestiona))
|
@if (!string.IsNullOrWhiteSpace(denuncia.ExpedienteGestionaMostrable))
|
||||||
{
|
{
|
||||||
<dt class="col-sm-3">Expediente Gestión</dt>
|
<dt class="col-sm-3">Nº expediente Gestiona</dt>
|
||||||
<dd class="col-sm-9">@denuncia.Expediente_Gestiona</dd>
|
<dd class="col-sm-9">@denuncia.ExpedienteGestionaMostrable</dd>
|
||||||
}
|
}
|
||||||
@if (denuncia.Id_Persona_Gestiona != 0)
|
@if (denuncia.Id_Persona_Gestiona != 0)
|
||||||
{
|
{
|
||||||
@@ -305,8 +307,6 @@ else
|
|||||||
// Variable para la búsqueda
|
// Variable para la búsqueda
|
||||||
private string busqueda = "";
|
private string busqueda = "";
|
||||||
|
|
||||||
private const string DENUNCIAS_JSON = @"C:\ZipsDenuncias\denuncias.json";
|
|
||||||
private const string FICHEROS_JSON = @"C:\ZipsDenuncias\ficheros.json";
|
|
||||||
private bool isLoading = false;
|
private bool isLoading = false;
|
||||||
private bool hasLoaded = false;
|
private bool hasLoaded = false;
|
||||||
|
|
||||||
@@ -314,11 +314,6 @@ else
|
|||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(userState.Token))
|
|
||||||
{
|
|
||||||
Navigation.NavigateTo("/", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
await CargarRechazadasAsync();
|
await CargarRechazadasAsync();
|
||||||
@@ -332,29 +327,21 @@ else
|
|||||||
private async Task CargarRechazadasAsync()
|
private async Task CargarRechazadasAsync()
|
||||||
{
|
{
|
||||||
var todas = await CargarDenunciasJsonAsync();
|
var todas = await CargarDenunciasJsonAsync();
|
||||||
denunciasRechazadas = todas.Where(d => d.Expediente_Gestiona == "Rechazada").ToList();
|
// Ahora filtramos por la bandera EnRechazada
|
||||||
|
denunciasRechazadas = todas
|
||||||
|
.Where(d => d.EnRechazada)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<List<DenunciasGestiona>> CargarDenunciasJsonAsync()
|
private async Task<List<DenunciasGestiona>> CargarDenunciasJsonAsync()
|
||||||
{
|
{
|
||||||
if (File.Exists(DENUNCIAS_JSON))
|
return await DenunciaStore.GetAllDenunciasAsync();
|
||||||
{
|
|
||||||
var json = await File.ReadAllTextAsync(DENUNCIAS_JSON);
|
|
||||||
var lista = Newtonsoft.Json.JsonConvert.DeserializeObject<List<DenunciasGestiona>>(json);
|
|
||||||
return lista ?? new List<DenunciasGestiona>();
|
|
||||||
}
|
|
||||||
return new List<DenunciasGestiona>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<List<FicherosDenuncias>> CargarFicherosJsonAsync()
|
private async Task<List<FicherosDenuncias>> CargarFicherosJsonAsync()
|
||||||
{
|
{
|
||||||
if (File.Exists(FICHEROS_JSON))
|
return await DenunciaStore.GetAllFicherosAsync();
|
||||||
{
|
|
||||||
var json = await File.ReadAllTextAsync(FICHEROS_JSON);
|
|
||||||
var lista = Newtonsoft.Json.JsonConvert.DeserializeObject<List<FicherosDenuncias>>(json);
|
|
||||||
return lista ?? new List<FicherosDenuncias>();
|
|
||||||
}
|
|
||||||
return new List<FicherosDenuncias>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CargarFicherosAdjuntosAsync()
|
private async Task CargarFicherosAdjuntosAsync()
|
||||||
@@ -378,3 +365,4 @@ else
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
<Router AppAssembly="typeof(Program).Assembly">
|
<CascadingAuthenticationState>
|
||||||
|
<Router AppAssembly="typeof(Program).Assembly">
|
||||||
<Found Context="routeData">
|
<Found Context="routeData">
|
||||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
|
||||||
|
<NotAuthorized>
|
||||||
|
<AuthRedirect />
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeRouteView>
|
||||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
</Found>
|
</Found>
|
||||||
</Router>
|
</Router>
|
||||||
|
</CascadingAuthenticationState>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@using System.Net.Http
|
@using System.Net.Http
|
||||||
@using System.Net.Http.Json
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@@ -8,3 +10,6 @@
|
|||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using GestionaDenunciasAN
|
@using GestionaDenunciasAN
|
||||||
@using GestionaDenunciasAN.Components
|
@using GestionaDenunciasAN.Components
|
||||||
|
@using GestionaDenunciasAN.Components.Layout
|
||||||
|
@using GestionaDenunciasAN.Models
|
||||||
|
@using GestionaDenunciasAN.Services
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace GestionaDenunciasAN.Configuration;
|
||||||
|
|
||||||
|
public sealed class ComplaintStorageOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "ComplaintStorage";
|
||||||
|
|
||||||
|
public string ConnectionString { get; set; } = string.Empty;
|
||||||
|
public bool AutoCreateSchema { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace GestionaDenunciasAN.Configuration;
|
||||||
|
|
||||||
|
public sealed class GlobalLeaksOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "GlobalLeaks";
|
||||||
|
|
||||||
|
public string BaseUrl { get; set; } = "https://prebuzon.antifraudeandalucia.es";
|
||||||
|
public int TimeoutSeconds { get; set; } = 120;
|
||||||
|
public int MaxDownloadBytes { get; set; } = 500 * 1024 * 1024;
|
||||||
|
}
|
||||||
@@ -15,14 +15,17 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Blazor.Bootstrap" Version="3.3.1" />
|
<PackageReference Include="Blazor.Bootstrap" Version="3.3.1" />
|
||||||
|
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
|
||||||
|
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="PdfSharpCore" Version="1.3.67" />
|
<PackageReference Include="PdfSharpCore" Version="1.3.67" />
|
||||||
<PackageReference Include="Serialize.Linq" Version="4.0.167" />
|
<PackageReference Include="Serialize.Linq" Version="4.0.167" />
|
||||||
<PackageReference Include="System.Drawing.Common" Version="10.0.0-preview.3.25173.2" />
|
<PackageReference Include="System.Drawing.Common" Version="10.0.0-preview.3.25173.2" />
|
||||||
|
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\bdAntifraude\bdAntifraude.csproj" />
|
<Content Include="Scripts\gestiondenuncias_schema.sql" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
34
Antifraude.Net/GestionaDenunciasAN/GestionaDenunciasAN.sln
Normal file
34
Antifraude.Net/GestionaDenunciasAN/GestionaDenunciasAN.sln
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.0.31903.59
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GestionaDenunciasAN", "GestionaDenunciasAN.csproj", "{45DE522E-CB7F-4865-8644-D1916065F48E}"
|
||||||
|
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
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{45DE522E-CB7F-4865-8644-D1916065F48E}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,393 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Helpers;
|
||||||
|
|
||||||
|
public static class GlobalLeaksJsonEnricher
|
||||||
|
{
|
||||||
|
public static void Enrich(DenunciasGestiona denuncia, string exportJson)
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(exportJson);
|
||||||
|
var root = document.RootElement;
|
||||||
|
|
||||||
|
if (denuncia.Id_Denuncia == 0 &&
|
||||||
|
root.TryGetProperty("progressive", out var progressiveElement) &&
|
||||||
|
progressiveElement.TryGetInt32(out var progressive))
|
||||||
|
{
|
||||||
|
denuncia.Id_Denuncia = progressive;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("creation_date", out var creationDateElement) &&
|
||||||
|
DateTimeOffset.TryParse(creationDateElement.GetString(), out var creationDate))
|
||||||
|
{
|
||||||
|
denuncia.Fecha = creationDate.LocalDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("status", out var statusElement) &&
|
||||||
|
string.IsNullOrWhiteSpace(denuncia.Estado))
|
||||||
|
{
|
||||||
|
denuncia.Estado = statusElement.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("label", out var labelElement) &&
|
||||||
|
string.IsNullOrWhiteSpace(denuncia.Etiqueta))
|
||||||
|
{
|
||||||
|
denuncia.Etiqueta = labelElement.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("identity_provided", out var identityProvidedElement) &&
|
||||||
|
identityProvidedElement.ValueKind is JsonValueKind.True or JsonValueKind.False)
|
||||||
|
{
|
||||||
|
denuncia.Confidencial = !identityProvidedElement.GetBoolean();
|
||||||
|
if (string.IsNullOrWhiteSpace(denuncia.Tipo_Denuncia))
|
||||||
|
{
|
||||||
|
denuncia.Tipo_Denuncia = denuncia.Confidencial
|
||||||
|
? "Anónima"
|
||||||
|
: "No anónima, deseo identificarme";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var answers = ReadAnswers(root);
|
||||||
|
ApplyAnswers(denuncia, answers);
|
||||||
|
MergeComments(denuncia, root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> ReadAnswers(JsonElement root)
|
||||||
|
{
|
||||||
|
var definitions = new Dictionary<string, FieldDefinition>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var answersByLabel = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (!root.TryGetProperty("questionnaires", out var questionnaires) ||
|
||||||
|
questionnaires.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return answersByLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var questionnaire in questionnaires.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (questionnaire.TryGetProperty("steps", out var steps) &&
|
||||||
|
steps.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var step in steps.EnumerateArray())
|
||||||
|
{
|
||||||
|
CollectDefinitions(step, definitions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!questionnaire.TryGetProperty("answers", out var answers) ||
|
||||||
|
answers.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var answerEntry in answers.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (!definitions.TryGetValue(answerEntry.Name, out var definition))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolved = ResolveAnswer(answerEntry.Value, definition);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolved))
|
||||||
|
{
|
||||||
|
answersByLabel[Normalize(definition.Label)] = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return answersByLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CollectDefinitions(JsonElement element, Dictionary<string, FieldDefinition> definitions)
|
||||||
|
{
|
||||||
|
if (element.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.TryGetProperty("id", out var idElement) &&
|
||||||
|
element.TryGetProperty("type", out var typeElement) &&
|
||||||
|
element.TryGetProperty("label", out var labelElement))
|
||||||
|
{
|
||||||
|
var id = idElement.GetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
definitions[id] = new FieldDefinition(
|
||||||
|
labelElement.GetString() ?? id,
|
||||||
|
typeElement.GetString() ?? string.Empty,
|
||||||
|
ParseOptions(element));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.TryGetProperty("children", out var children) &&
|
||||||
|
children.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var child in children.EnumerateArray())
|
||||||
|
{
|
||||||
|
CollectDefinitions(child, definitions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> ParseOptions(JsonElement element)
|
||||||
|
{
|
||||||
|
var options = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (!element.TryGetProperty("options", out var optionsElement) ||
|
||||||
|
optionsElement.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var option in optionsElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (!option.TryGetProperty("id", out var idElement) ||
|
||||||
|
!option.TryGetProperty("label", out var labelElement))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = idElement.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
options[id] = labelElement.GetString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveAnswer(JsonElement answerArray, FieldDefinition definition)
|
||||||
|
{
|
||||||
|
if (answerArray.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var values = new List<string>();
|
||||||
|
|
||||||
|
foreach (var answer in answerArray.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (answer.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (answer.TryGetProperty("value", out var valueElement))
|
||||||
|
{
|
||||||
|
var resolvedValue = ResolveValue(valueElement, definition.Options);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolvedValue))
|
||||||
|
{
|
||||||
|
values.Add(resolvedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in answer.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (property.NameEquals("value") || property.NameEquals("index") || property.NameEquals("required_status"))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.Value.ValueKind is JsonValueKind.True or JsonValueKind.False &&
|
||||||
|
property.Value.GetBoolean() &&
|
||||||
|
definition.Options.TryGetValue(property.Name, out var label))
|
||||||
|
{
|
||||||
|
values.Add(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join("; ", values.Distinct(StringComparer.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveValue(JsonElement valueElement, Dictionary<string, string> options)
|
||||||
|
{
|
||||||
|
return valueElement.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.String => ResolveStringValue(valueElement.GetString(), options),
|
||||||
|
JsonValueKind.True => "Sí",
|
||||||
|
JsonValueKind.False => "No",
|
||||||
|
JsonValueKind.Number => valueElement.GetRawText(),
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveStringValue(string? rawValue, Dictionary<string, string> options)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawValue))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options.TryGetValue(rawValue, out var optionLabel)
|
||||||
|
? optionLabel
|
||||||
|
: rawValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyAnswers(DenunciasGestiona denuncia, Dictionary<string, string> answers)
|
||||||
|
{
|
||||||
|
SetIfMissing(() => denuncia.TipoDenunciante, value => denuncia.TipoDenunciante = value, answers, "indique si actua como persona fisica o en representacion de una persona juridica");
|
||||||
|
if (!string.IsNullOrWhiteSpace(denuncia.TipoDenunciante))
|
||||||
|
{
|
||||||
|
denuncia.EsPersonaJuridica = Normalize(denuncia.TipoDenunciante).Contains("juridica", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetIfMissing(() => denuncia.Asunto, value => denuncia.Asunto = value, answers, "asunto");
|
||||||
|
SetIfMissing(() => denuncia.Nombre, value => denuncia.Nombre = value, answers, "nombre");
|
||||||
|
SetIfMissing(() => denuncia.PrimerApellido, value => denuncia.PrimerApellido = value, answers, "1 apellido", "primer apellido");
|
||||||
|
SetIfMissing(() => denuncia.SegundoApellido, value => denuncia.SegundoApellido = value, answers, "2 apellido", "segundo apellido");
|
||||||
|
SetIfMissing(() => denuncia.Apellidos, value => denuncia.Apellidos = value, answers, "apellidos");
|
||||||
|
SetIfMissing(() => denuncia.RazonSocial, value => denuncia.RazonSocial = value, answers, "razon social");
|
||||||
|
SetIfMissing(() => denuncia.Sexo, value => denuncia.Sexo = value, answers, "sexo");
|
||||||
|
SetIfMissing(() => denuncia.PaisOrigen, value => denuncia.PaisOrigen = value, answers, "pais de origen");
|
||||||
|
SetIfMissing(() => denuncia.Dni, value => denuncia.Dni = value, answers, "dni", "nif", "nie", "cif", "otro documento identificativo");
|
||||||
|
SetIfMissing(() => denuncia.A_Quien_Denuncia, value => denuncia.A_Quien_Denuncia = value, answers, "a quien denuncia");
|
||||||
|
SetIfMissing(() => denuncia.DenunciadoDetalle, value => denuncia.DenunciadoDetalle = value, answers, "especifique a quien denuncia");
|
||||||
|
SetIfMissing(() => denuncia.Descripcion_Denuncia, value => denuncia.Descripcion_Denuncia = value, answers, "describa su denuncia", "descripcion de la denuncia");
|
||||||
|
SetIfMissing(() => denuncia.Denunciado_Ante_Inst, value => denuncia.Denunciado_Ante_Inst = value, answers, "ha denunciado estos hechos ante otras instituciones u organos");
|
||||||
|
SetIfMissing(() => denuncia.OrganismoDenunciado, value => denuncia.OrganismoDenunciado = value, answers, "por favor indique el organismo o la institucion donde ha denunciado los hechos");
|
||||||
|
SetIfMissing(() => denuncia.SolicitaProteccion, value => denuncia.SolicitaProteccion = value, answers, "solicita medidas concretas de proteccion");
|
||||||
|
SetIfMissing(() => denuncia.MedidasProteccionSolicitadas, value => denuncia.MedidasProteccionSolicitadas = value, answers, "describa las medidas de proteccion solicitadas");
|
||||||
|
SetIfMissing(() => denuncia.Lugar_Hechos, value => denuncia.Lugar_Hechos = value, answers, "lugar en la que ocurrieron los hechos que denuncia");
|
||||||
|
SetIfMissing(() => denuncia.AutorizaRemision, value => denuncia.AutorizaRemision = value, answers, "autorizacion para remitir su denuncia");
|
||||||
|
SetIfMissing(() => denuncia.PreferenciaRemision, value => denuncia.PreferenciaRemision = value, answers, "en tal caso desea que su denuncia se remita anonimizada sin datos personales");
|
||||||
|
SetIfMissing(() => denuncia.Notificacion_Preferencia, value => denuncia.Notificacion_Preferencia = value, answers, "seleccione su preferencia de notificacion y seguimiento de su denuncia", "notificaciones");
|
||||||
|
SetIfMissing(() => denuncia.Notificacion_Electronica, value => denuncia.Notificacion_Electronica = value, answers, "notificaciones electronicas");
|
||||||
|
SetIfMissing(() => denuncia.SeguimientoOnline, value => denuncia.SeguimientoOnline = value, answers, "seguimiento online");
|
||||||
|
SetIfMissing(() => denuncia.NotificacionPostal, value => denuncia.NotificacionPostal = value, answers, "autorizo recibir notificaciones via correo postal");
|
||||||
|
SetIfMissing(() => denuncia.Correo_Electronico, value => denuncia.Correo_Electronico = value, answers, "correo electronico", "email");
|
||||||
|
SetIfMissing(() => denuncia.Telefono, value => denuncia.Telefono = value, answers, "telefono", "telefono movil");
|
||||||
|
SetIfMissing(() => denuncia.Direccion, value => denuncia.Direccion = value, answers, "nombre de la via", "direccion", "domicilio");
|
||||||
|
SetIfMissing(() => denuncia.DireccionTipoVia, value => denuncia.DireccionTipoVia = value, answers, "tipo de via");
|
||||||
|
SetIfMissing(() => denuncia.DireccionNumero, value => denuncia.DireccionNumero = value, answers, "numero", "numero km");
|
||||||
|
SetIfMissing(() => denuncia.DireccionPiso, value => denuncia.DireccionPiso = value, answers, "piso", "planta");
|
||||||
|
SetIfMissing(() => denuncia.DireccionPuerta, value => denuncia.DireccionPuerta = value, answers, "puerta");
|
||||||
|
SetIfMissing(() => denuncia.DireccionBloque, value => denuncia.DireccionBloque = value, answers, "bloque");
|
||||||
|
SetIfMissing(() => denuncia.DireccionEscalera, value => denuncia.DireccionEscalera = value, answers, "escalera");
|
||||||
|
SetIfMissing(() => denuncia.DireccionExtra, value => denuncia.DireccionExtra = value, answers, "extra");
|
||||||
|
SetIfMissing(() => denuncia.Municipio, value => denuncia.Municipio = value, answers, "municipio", "localidad", "poblacion");
|
||||||
|
SetIfMissing(() => denuncia.Provincia, value => denuncia.Provincia = value, answers, "provincia");
|
||||||
|
SetIfMissing(() => denuncia.CodigoPostal, value => denuncia.CodigoPostal = value, answers, "codigo postal", "c p");
|
||||||
|
SetIfMissing(() => denuncia.Pais, value => denuncia.Pais = value, answers, "pais");
|
||||||
|
|
||||||
|
if (denuncia.Fecha_Hechos == DateTime.MinValue &&
|
||||||
|
TryGetAnswer(answers, out var fechaHechos, "fecha de los hechos que denuncia") &&
|
||||||
|
DateTime.TryParse(fechaHechos, CultureInfo.CurrentCulture, DateTimeStyles.None, out var parsedDate))
|
||||||
|
{
|
||||||
|
denuncia.Fecha_Hechos = parsedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(denuncia.Apellidos))
|
||||||
|
{
|
||||||
|
denuncia.Apellidos = string.Join(
|
||||||
|
' ',
|
||||||
|
new[] { denuncia.PrimerApellido, denuncia.SegundoApellido }
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void MergeComments(DenunciasGestiona denuncia, JsonElement root)
|
||||||
|
{
|
||||||
|
if (!root.TryGetProperty("comments", out var commentsElement) ||
|
||||||
|
commentsElement.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var comment in commentsElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (!comment.TryGetProperty("content", out var contentElement))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = contentElement.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builder.Length > 0)
|
||||||
|
{
|
||||||
|
builder.AppendLine().AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(content.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
var merged = builder.ToString().Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(merged) && !denuncia.Comments.Contains(merged, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
denuncia.Comments = string.IsNullOrWhiteSpace(denuncia.Comments)
|
||||||
|
? merged
|
||||||
|
: $"{denuncia.Comments}{Environment.NewLine}{Environment.NewLine}{merged}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetIfMissing(
|
||||||
|
Func<string> currentValue,
|
||||||
|
Action<string> assign,
|
||||||
|
Dictionary<string, string> answers,
|
||||||
|
params string[] candidateLabels)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(currentValue()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetAnswer(answers, out var value, candidateLabels))
|
||||||
|
{
|
||||||
|
assign(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetAnswer(
|
||||||
|
Dictionary<string, string> answers,
|
||||||
|
out string value,
|
||||||
|
params string[] candidateLabels)
|
||||||
|
{
|
||||||
|
foreach (var label in candidateLabels)
|
||||||
|
{
|
||||||
|
if (answers.TryGetValue(Normalize(label), out value!))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = text.Normalize(NormalizationForm.FormD);
|
||||||
|
var builder = new StringBuilder(normalized.Length);
|
||||||
|
|
||||||
|
foreach (var character in normalized)
|
||||||
|
{
|
||||||
|
var category = CharUnicodeInfo.GetUnicodeCategory(character);
|
||||||
|
if (category == UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(' ', builder.ToString().Split(' ', StringSplitOptions.RemoveEmptyEntries));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record FieldDefinition(string Label, string Type, Dictionary<string, string> Options);
|
||||||
|
}
|
||||||
@@ -153,5 +153,8 @@ namespace GestionaDenunciasAN.Helpers
|
|||||||
outputDoc.Save(ms);
|
outputDoc.Save(ms);
|
||||||
return ms.ToArray();
|
return ms.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
464
Antifraude.Net/GestionaDenunciasAN/Helpers/ReportParser.cs
Normal file
464
Antifraude.Net/GestionaDenunciasAN/Helpers/ReportParser.cs
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Helpers;
|
||||||
|
|
||||||
|
public static class ReportParser
|
||||||
|
{
|
||||||
|
public static DenunciasGestiona ParseReport(string reportText)
|
||||||
|
{
|
||||||
|
var lines = NormalizeLines(reportText);
|
||||||
|
var denuncia = new DenunciasGestiona
|
||||||
|
{
|
||||||
|
TextoOriginalReport = reportText ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
ParseMetadata(lines, denuncia);
|
||||||
|
|
||||||
|
var fields = ParseFormFields(lines, out var comments);
|
||||||
|
denuncia.SetCamposFormulario(fields);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(comments))
|
||||||
|
{
|
||||||
|
denuncia.Comments = comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyFieldMappings(denuncia, fields);
|
||||||
|
FinalizeReporterData(denuncia);
|
||||||
|
return denuncia;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] NormalizeLines(string? reportText)
|
||||||
|
{
|
||||||
|
return (reportText ?? string.Empty)
|
||||||
|
.Replace("\r\n", "\n")
|
||||||
|
.Replace('\r', '\n')
|
||||||
|
.Split('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseMetadata(string[] lines, DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
foreach (var rawLine in lines)
|
||||||
|
{
|
||||||
|
var line = rawLine.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith("ID:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (int.TryParse(line[3..].Trim(), out var id))
|
||||||
|
{
|
||||||
|
denuncia.Id_Denuncia = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith("Fecha:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var value = line[6..].Trim();
|
||||||
|
if (value.EndsWith("(UTC)", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = value[..^5].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.TryParseExact(
|
||||||
|
value,
|
||||||
|
"dddd dd MMMM yyyy HH:mm",
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||||
|
out var parsedDate))
|
||||||
|
{
|
||||||
|
denuncia.Fecha = parsedDate;
|
||||||
|
}
|
||||||
|
else if (DateTime.TryParse(
|
||||||
|
value,
|
||||||
|
CultureInfo.InvariantCulture,
|
||||||
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||||
|
out parsedDate))
|
||||||
|
{
|
||||||
|
denuncia.Fecha = parsedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith("Etiqueta:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
denuncia.Etiqueta = line[9..].Trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.StartsWith("Estado:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
denuncia.Estado = line[7..].Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ReportFieldEntry> ParseFormFields(string[] lines, out string comments)
|
||||||
|
{
|
||||||
|
var fields = new List<ReportFieldEntry>();
|
||||||
|
var commentBuilder = new StringBuilder();
|
||||||
|
var currentSection = string.Empty;
|
||||||
|
var order = 0;
|
||||||
|
var messagesSection = false;
|
||||||
|
|
||||||
|
for (var i = 0; i < lines.Length; i++)
|
||||||
|
{
|
||||||
|
var rawLine = lines[i];
|
||||||
|
var trimmed = rawLine.Trim();
|
||||||
|
|
||||||
|
if (messagesSection)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(trimmed))
|
||||||
|
{
|
||||||
|
if (commentBuilder.Length > 0)
|
||||||
|
{
|
||||||
|
commentBuilder.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
commentBuilder.Append(trimmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(trimmed) || IsMetadataLine(trimmed))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(trimmed, "{Messages}", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
messagesSection = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var indent = CountIndentation(rawLine);
|
||||||
|
if (indent == 0)
|
||||||
|
{
|
||||||
|
currentSection = trimmed;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var label = trimmed;
|
||||||
|
var valueLines = new List<string>();
|
||||||
|
var j = i + 1;
|
||||||
|
|
||||||
|
while (j < lines.Length)
|
||||||
|
{
|
||||||
|
var nextRaw = lines[j];
|
||||||
|
var nextTrimmed = nextRaw.Trim();
|
||||||
|
|
||||||
|
if (string.Equals(nextTrimmed, "{Messages}", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(nextTrimmed))
|
||||||
|
{
|
||||||
|
j++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextIndent = CountIndentation(nextRaw);
|
||||||
|
if (nextIndent <= indent)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
valueLines.Add(nextTrimmed);
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.Add(new ReportFieldEntry
|
||||||
|
{
|
||||||
|
Order = ++order,
|
||||||
|
Section = currentSection,
|
||||||
|
Label = label,
|
||||||
|
Value = string.Join(Environment.NewLine, valueLines)
|
||||||
|
});
|
||||||
|
|
||||||
|
i = j - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
comments = commentBuilder.ToString().Trim();
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyFieldMappings(DenunciasGestiona denuncia, IReadOnlyList<ReportFieldEntry> fields)
|
||||||
|
{
|
||||||
|
var lookup = fields
|
||||||
|
.GroupBy(field => Normalize(field.Label))
|
||||||
|
.ToDictionary(
|
||||||
|
group => group.Key,
|
||||||
|
group => group
|
||||||
|
.Select(entry => entry.Value.Trim())
|
||||||
|
.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
denuncia.TipoDenunciante = FindValue(
|
||||||
|
lookup,
|
||||||
|
"indique si actua como persona fisica o en representacion de una persona juridica");
|
||||||
|
denuncia.EsPersonaJuridica = denuncia.TipoDenunciante.Contains("jurid", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
denuncia.Nombre = FindValue(lookup, "nombre");
|
||||||
|
denuncia.PrimerApellido = FindValue(lookup, "1 apellido", "primer apellido");
|
||||||
|
denuncia.SegundoApellido = FindValue(lookup, "2 apellido", "segundo apellido");
|
||||||
|
denuncia.Apellidos = string.Join(
|
||||||
|
' ',
|
||||||
|
new[] { denuncia.PrimerApellido, denuncia.SegundoApellido }
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||||
|
denuncia.RazonSocial = FindValue(lookup, "razon social");
|
||||||
|
denuncia.Sexo = NormalizeGender(FindValue(lookup, "sexo"));
|
||||||
|
denuncia.Telefono = FindValue(lookup, "contacto telefonico", "telefono", "telefono movil");
|
||||||
|
denuncia.PaisOrigen = FindValue(lookup, "pais de origen");
|
||||||
|
|
||||||
|
if (TryGetValue(lookup, out var identifier, "nif dni nie"))
|
||||||
|
{
|
||||||
|
denuncia.Dni = identifier;
|
||||||
|
denuncia.TipoDocumentoIdentificativo = "NIF (DNI, NIE)";
|
||||||
|
}
|
||||||
|
else if (TryGetValue(lookup, out identifier, "cif"))
|
||||||
|
{
|
||||||
|
denuncia.Dni = identifier;
|
||||||
|
denuncia.TipoDocumentoIdentificativo = "CIF";
|
||||||
|
}
|
||||||
|
else if (TryGetValue(lookup, out identifier, "otro documento identificativo"))
|
||||||
|
{
|
||||||
|
denuncia.Dni = identifier;
|
||||||
|
denuncia.TipoDocumentoIdentificativo = "Otro documento identificativo";
|
||||||
|
}
|
||||||
|
|
||||||
|
denuncia.Asunto = FindValue(lookup, "asunto");
|
||||||
|
denuncia.A_Quien_Denuncia = FindValue(lookup, "a quien denuncia");
|
||||||
|
denuncia.DenunciadoDetalle = FindValue(lookup, "especifique a quien denuncia");
|
||||||
|
denuncia.Descripcion_Denuncia = FindValue(lookup, "describa su denuncia");
|
||||||
|
denuncia.Denunciado_Ante_Inst = FindValue(
|
||||||
|
lookup,
|
||||||
|
"ha denunciado estos hechos ante otras instituciones u organos");
|
||||||
|
denuncia.OrganismoDenunciado = FindValue(
|
||||||
|
lookup,
|
||||||
|
"por favor indique el organismo o la institucion donde ha denunciado los hechos",
|
||||||
|
"por favor indique el organismo o la institucion donde ha denunciado los hechos");
|
||||||
|
denuncia.SolicitaProteccion = FindValue(
|
||||||
|
lookup,
|
||||||
|
"solicita medidas concretas de proteccion");
|
||||||
|
denuncia.MedidasProteccionSolicitadas = FindValue(
|
||||||
|
lookup,
|
||||||
|
"describa las medidas de proteccion solicitadas");
|
||||||
|
denuncia.Lugar_Hechos = FindValue(
|
||||||
|
lookup,
|
||||||
|
"lugar en el que ocurrieron los hechos que denuncia",
|
||||||
|
"lugar en la que ocurrieron los hechos que denuncia");
|
||||||
|
|
||||||
|
var fechaHechos = FindValue(lookup, "fecha de los hechos que denuncia");
|
||||||
|
if (TryParseSpanishDate(fechaHechos, out var parsedFactsDate))
|
||||||
|
{
|
||||||
|
denuncia.Fecha_Hechos = parsedFactsDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
denuncia.AutorizaRemision = FindValue(lookup, "autorizacion para remitir su denuncia");
|
||||||
|
denuncia.PreferenciaRemision = FindValue(
|
||||||
|
lookup,
|
||||||
|
"en tal caso desea que su denuncia se remita anonimizada sin datos personales");
|
||||||
|
|
||||||
|
denuncia.Notificacion_Preferencia = FindValue(lookup, "preferencia de notificacion");
|
||||||
|
denuncia.Notificacion_Electronica = FindValue(lookup, "notificaciones electronicas");
|
||||||
|
denuncia.SeguimientoOnline = FindValue(lookup, "seguimiento online");
|
||||||
|
denuncia.NotificacionPostal = FindValue(lookup, "autorizo recibir notificaciones via correo postal");
|
||||||
|
denuncia.Correo_Electronico = FindValue(lookup, "correo electronico");
|
||||||
|
denuncia.Provincia = FindValue(lookup, "provincia");
|
||||||
|
denuncia.DireccionTipoVia = NormalizeRoadType(FindValue(lookup, "tipo de via"));
|
||||||
|
denuncia.Direccion = FindValue(lookup, "nombre de la via", "direccion", "domicilio");
|
||||||
|
denuncia.CodigoPostal = FindValue(lookup, "codigo postal", "c p");
|
||||||
|
denuncia.Municipio = FindValue(lookup, "localidad", "municipio", "poblacion");
|
||||||
|
denuncia.DireccionNumero = FindValue(lookup, "numero km", "numero");
|
||||||
|
denuncia.DireccionBloque = FindValue(lookup, "bloque");
|
||||||
|
denuncia.DireccionEscalera = FindValue(lookup, "escalera");
|
||||||
|
denuncia.DireccionPiso = FindValue(lookup, "planta", "piso");
|
||||||
|
denuncia.DireccionPuerta = FindValue(lookup, "puerta");
|
||||||
|
denuncia.DireccionExtra = FindValue(lookup, "extra");
|
||||||
|
|
||||||
|
denuncia.Condiciones = fields.Any(field =>
|
||||||
|
field.Section.Contains("Condiciones", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
(field.Value.Contains("☑", StringComparison.Ordinal) ||
|
||||||
|
field.Value.Contains("Acepto", StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(denuncia.Pais))
|
||||||
|
{
|
||||||
|
denuncia.Pais = denuncia.PaisOrigen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FinalizeReporterData(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(denuncia.TipoDenunciante))
|
||||||
|
{
|
||||||
|
denuncia.TipoDenunciante = string.IsNullOrWhiteSpace(denuncia.RazonSocial) &&
|
||||||
|
string.IsNullOrWhiteSpace(denuncia.Nombre)
|
||||||
|
? "Anonima"
|
||||||
|
: "Persona fisica";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (denuncia.EsPersonaJuridica && string.IsNullOrWhiteSpace(denuncia.RazonSocial))
|
||||||
|
{
|
||||||
|
denuncia.RazonSocial = denuncia.NombreDenuncianteMostrar;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(denuncia.Tipo_Denuncia))
|
||||||
|
{
|
||||||
|
denuncia.Tipo_Denuncia = string.IsNullOrWhiteSpace(denuncia.RazonSocial) &&
|
||||||
|
string.IsNullOrWhiteSpace(denuncia.Nombre)
|
||||||
|
? "Anonima"
|
||||||
|
: "No anonima, deseo identificarme";
|
||||||
|
}
|
||||||
|
|
||||||
|
denuncia.Confidencial = DenunciasGestiona.IndicaDenunciaAnonima(denuncia.Tipo_Denuncia) ||
|
||||||
|
!DenunciasGestiona.IndicaDenunciaIdentificada(denuncia.Tipo_Denuncia) &&
|
||||||
|
string.IsNullOrWhiteSpace(denuncia.RazonSocialResuelta) &&
|
||||||
|
string.IsNullOrWhiteSpace(denuncia.NombreResuelto) &&
|
||||||
|
string.IsNullOrWhiteSpace(denuncia.DocumentoResuelto);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindValue(
|
||||||
|
IReadOnlyDictionary<string, string> lookup,
|
||||||
|
params string[] candidateLabels)
|
||||||
|
{
|
||||||
|
return TryGetValue(lookup, out var value, candidateLabels) ? value : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetValue(
|
||||||
|
IReadOnlyDictionary<string, string> lookup,
|
||||||
|
out string value,
|
||||||
|
params string[] candidateLabels)
|
||||||
|
{
|
||||||
|
foreach (var candidateLabel in candidateLabels)
|
||||||
|
{
|
||||||
|
if (lookup.TryGetValue(Normalize(candidateLabel), out value!) &&
|
||||||
|
!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseSpanishDate(string? value, out DateTime result)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
result = DateTime.MinValue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.TryParseExact(
|
||||||
|
value,
|
||||||
|
new[] { "dd/MM/yyyy", "d/M/yyyy", "dd-MM-yyyy", "d-M-yyyy" },
|
||||||
|
CultureInfo.GetCultureInfo("es-ES"),
|
||||||
|
DateTimeStyles.None,
|
||||||
|
out result))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.TryParse(value, CultureInfo.GetCultureInfo("es-ES"), DateTimeStyles.None, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeGender(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Normalize(value) switch
|
||||||
|
{
|
||||||
|
"masculino" or "hombre" => "Hombre",
|
||||||
|
"femenino" or "mujer" => "Mujer",
|
||||||
|
_ => value.Trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeRoadType(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = Normalize(value);
|
||||||
|
return normalized switch
|
||||||
|
{
|
||||||
|
"calle" => "CL",
|
||||||
|
"avenida" => "AV",
|
||||||
|
"plaza" => "PZ",
|
||||||
|
"carretera" => "CR",
|
||||||
|
"camino" => "CM",
|
||||||
|
"poligono" => "PG",
|
||||||
|
_ => value.Trim().ToUpperInvariant()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMetadataLine(string line)
|
||||||
|
{
|
||||||
|
return line.StartsWith("ID:", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
line.StartsWith("Fecha:", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
line.StartsWith("Etiqueta:", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
line.StartsWith("Estado:", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountIndentation(string value)
|
||||||
|
{
|
||||||
|
var indent = 0;
|
||||||
|
foreach (var character in value)
|
||||||
|
{
|
||||||
|
if (character == ' ')
|
||||||
|
{
|
||||||
|
indent++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (character == '\t')
|
||||||
|
{
|
||||||
|
indent += 4;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Normalize(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = value.Normalize(NormalizationForm.FormD);
|
||||||
|
var builder = new StringBuilder(normalized.Length);
|
||||||
|
|
||||||
|
foreach (var character in normalized)
|
||||||
|
{
|
||||||
|
var category = CharUnicodeInfo.GetUnicodeCategory(character);
|
||||||
|
if (category == UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Regex.Replace(builder.ToString(), @"\s+", " ").Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Antifraude.Net/GestionaDenunciasAN/Models/ApiError.cs
Normal file
3
Antifraude.Net/GestionaDenunciasAN/Models/ApiError.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed record ApiError(string Error, bool SessionExpired = false);
|
||||||
3
Antifraude.Net/GestionaDenunciasAN/Models/ContextDto.cs
Normal file
3
Antifraude.Net/GestionaDenunciasAN/Models/ContextDto.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed record ContextDto(string Id, string Name);
|
||||||
@@ -1,108 +1,282 @@
|
|||||||
using System;
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
namespace GestionaDenunciasAN.Models
|
|
||||||
{
|
|
||||||
public class DenunciasGestiona
|
public class DenunciasGestiona
|
||||||
{
|
{
|
||||||
// Propiedades existentes
|
|
||||||
public int Id_RegistroDenuncia { get; set; }
|
public int Id_RegistroDenuncia { get; set; }
|
||||||
public int Id_Denuncia { get; set; }
|
public int Id_Denuncia { get; set; }
|
||||||
public DateTime Fecha { get; set; }
|
public DateTime Fecha { get; set; } = DateTime.MinValue;
|
||||||
public string Expediente_Gestiona { get; set; }
|
public string Expediente_Gestiona { get; set; } = string.Empty;
|
||||||
|
public string CodigoExpedienteGestiona { get; set; } = string.Empty;
|
||||||
public int Id_Persona_Gestiona { get; set; }
|
public int Id_Persona_Gestiona { get; set; }
|
||||||
public string Etiqueta { get; set; }
|
public string Etiqueta { get; set; } = string.Empty;
|
||||||
public string Estado { get; set; }
|
public string Estado { get; set; } = string.Empty;
|
||||||
public string Tipo_Denuncia { get; set; }
|
|
||||||
public string? Nombre { get; set; }
|
public string Tipo_Denuncia { get; set; } = string.Empty;
|
||||||
public string? Apellidos { get; set; }
|
public string TipoDenunciante { get; set; } = string.Empty;
|
||||||
public string? Sexo { get; set; }
|
public bool EsPersonaJuridica { get; set; }
|
||||||
public string? Dni { get; set; }
|
public string Nombre { get; set; } = string.Empty;
|
||||||
public string Asunto { get; set; }
|
public string PrimerApellido { get; set; } = string.Empty;
|
||||||
public string A_Quien_Denuncia { get; set; }
|
public string SegundoApellido { get; set; } = string.Empty;
|
||||||
public string Descripcion_Denuncia { get; set; }
|
public string Apellidos { get; set; } = string.Empty;
|
||||||
public string Denunciado_Ante_Inst { get; set; }
|
public string RazonSocial { get; set; } = string.Empty;
|
||||||
public string? Modalidad_Informacion { get; set; }
|
public string Sexo { get; set; } = string.Empty;
|
||||||
public string Lugar_Hechos { get; set; }
|
public string Dni { get; set; } = string.Empty;
|
||||||
public DateTime Fecha_Hechos { get; set; }
|
public string TipoDocumentoIdentificativo { get; set; } = string.Empty;
|
||||||
public string? Notificacion_Preferencia { get; set; }
|
public string PaisOrigen { get; set; } = string.Empty;
|
||||||
public string? Notificacion_Electronica { get; set; }
|
public string Asunto { get; set; } = string.Empty;
|
||||||
public string? Correo_Electronico { get; set; }
|
public string A_Quien_Denuncia { get; set; } = string.Empty;
|
||||||
public string? Notificacion_Sms { get; set; }
|
public string DenunciadoDetalle { get; set; } = string.Empty;
|
||||||
|
public string Descripcion_Denuncia { get; set; } = string.Empty;
|
||||||
|
public string Denunciado_Ante_Inst { get; set; } = string.Empty;
|
||||||
|
public string OrganismoDenunciado { get; set; } = string.Empty;
|
||||||
|
public string SolicitaProteccion { get; set; } = string.Empty;
|
||||||
|
public string MedidasProteccionSolicitadas { get; set; } = string.Empty;
|
||||||
|
public string Modalidad_Informacion { get; set; } = string.Empty;
|
||||||
|
public string Lugar_Hechos { get; set; } = string.Empty;
|
||||||
|
public DateTime Fecha_Hechos { get; set; } = DateTime.MinValue;
|
||||||
|
public string AutorizaRemision { get; set; } = string.Empty;
|
||||||
|
public string PreferenciaRemision { get; set; } = string.Empty;
|
||||||
|
public string Notificacion_Preferencia { get; set; } = string.Empty;
|
||||||
|
public string Notificacion_Electronica { get; set; } = string.Empty;
|
||||||
|
public string SeguimientoOnline { get; set; } = string.Empty;
|
||||||
|
public string NotificacionPostal { get; set; } = string.Empty;
|
||||||
|
public string Correo_Electronico { get; set; } = string.Empty;
|
||||||
|
public string Notificacion_Sms { get; set; } = string.Empty;
|
||||||
public bool Condiciones { get; set; }
|
public bool Condiciones { get; set; }
|
||||||
public string? Comments { get; set; }
|
public string Comments { get; set; } = string.Empty;
|
||||||
|
public string Telefono { get; set; } = string.Empty;
|
||||||
|
public string Direccion { get; set; } = string.Empty;
|
||||||
|
public string DireccionTipoVia { get; set; } = string.Empty;
|
||||||
|
public string DireccionNumero { get; set; } = string.Empty;
|
||||||
|
public string DireccionPiso { get; set; } = string.Empty;
|
||||||
|
public string DireccionPuerta { get; set; } = string.Empty;
|
||||||
|
public string DireccionBloque { get; set; } = string.Empty;
|
||||||
|
public string DireccionEscalera { get; set; } = string.Empty;
|
||||||
|
public string DireccionExtra { get; set; } = string.Empty;
|
||||||
|
public string Municipio { get; set; } = string.Empty;
|
||||||
|
public string Provincia { get; set; } = string.Empty;
|
||||||
|
public string CodigoPostal { get; set; } = string.Empty;
|
||||||
|
public string Pais { get; set; } = string.Empty;
|
||||||
|
public string CamposFormularioJson { get; set; } = string.Empty;
|
||||||
|
public string TextoOriginalReport { get; set; } = string.Empty;
|
||||||
|
|
||||||
// Nuevas propiedades
|
public bool Confidencial { get; set; }
|
||||||
public string NombreDenuncia { get; set; }
|
|
||||||
public string EstadoDenuncia { get; set; }
|
|
||||||
public string ArchivoElegido { get; set; }
|
|
||||||
public DateTime FechaSubidaAGestiona { get; set; } // Nueva propiedad
|
|
||||||
|
|
||||||
// Constructor por defecto
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
public DenunciasGestiona()
|
public bool EsActualizacion { get; set; }
|
||||||
|
|
||||||
|
public Guid ProcedureId { get; set; } = Guid.Empty;
|
||||||
|
public Guid GroupId { get; set; } = Guid.Empty;
|
||||||
|
|
||||||
|
public string NombreDenuncia { get; set; } = string.Empty;
|
||||||
|
public string EstadoDenuncia { get; set; } = string.Empty;
|
||||||
|
public string ArchivoElegido { get; set; } = string.Empty;
|
||||||
|
public DateTime FechaSubidaAGestiona { get; set; } = DateTime.MinValue;
|
||||||
|
|
||||||
|
public bool EnGestiona { get; set; }
|
||||||
|
public bool EnRechazada { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string NombreResuelto => ResolveValue(Nombre, "nombre");
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string PrimerApellidoResuelto => ResolveValue(PrimerApellido, "1 apellido", "primer apellido");
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string SegundoApellidoResuelto => ResolveValue(SegundoApellido, "2 apellido", "segundo apellido");
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string ApellidosResueltos
|
||||||
{
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(Apellidos))
|
||||||
|
{
|
||||||
|
return Apellidos.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constructor parametrizado que inicializa todos los campos, incluida la nueva propiedad
|
return string.Join(
|
||||||
public DenunciasGestiona(
|
' ',
|
||||||
int idRegistroDenuncia,
|
new[] { PrimerApellidoResuelto, SegundoApellidoResuelto }
|
||||||
int idDenuncia,
|
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||||
DateTime fecha,
|
}
|
||||||
string expedienteGestiona,
|
}
|
||||||
int idPersonaGestiona,
|
|
||||||
string etiqueta,
|
[JsonIgnore]
|
||||||
string estado,
|
public string RazonSocialResuelta => ResolveValue(RazonSocial, "razon social");
|
||||||
string tipoDenuncia,
|
|
||||||
string? nombre,
|
[JsonIgnore]
|
||||||
string? apellidos,
|
public string DocumentoResuelto => ResolveValue(
|
||||||
string? sexo,
|
Dni,
|
||||||
string? dni,
|
"nif dni nie",
|
||||||
string asunto,
|
"dni",
|
||||||
string aQuienDenuncia,
|
"nif",
|
||||||
string descripcionDenuncia,
|
"nie",
|
||||||
string denunciadoAnteInst,
|
"cif",
|
||||||
string? modalidadInformacion,
|
"otro documento identificativo");
|
||||||
string lugarHechos,
|
|
||||||
DateTime fechaHechos,
|
[JsonIgnore]
|
||||||
string? notificacionPreferencia,
|
public bool EsAnonima
|
||||||
string? notificacionElectronica,
|
|
||||||
string? correoElectronico,
|
|
||||||
string? notificacionSms,
|
|
||||||
bool condiciones,
|
|
||||||
string? comments,
|
|
||||||
string nombreDenuncia,
|
|
||||||
string estadoDenuncia,
|
|
||||||
string archivoElegido,
|
|
||||||
DateTime fechaSubidaAGestiona)
|
|
||||||
{
|
{
|
||||||
Id_RegistroDenuncia = idRegistroDenuncia;
|
get
|
||||||
Id_Denuncia = idDenuncia;
|
{
|
||||||
Fecha = fecha;
|
if (IndicaDenunciaIdentificada(Tipo_Denuncia))
|
||||||
Expediente_Gestiona = expedienteGestiona;
|
{
|
||||||
Id_Persona_Gestiona = idPersonaGestiona;
|
return false;
|
||||||
Etiqueta = etiqueta;
|
}
|
||||||
Estado = estado;
|
|
||||||
Tipo_Denuncia = tipoDenuncia;
|
if (Confidencial)
|
||||||
Nombre = nombre;
|
{
|
||||||
Apellidos = apellidos;
|
return true;
|
||||||
Sexo = sexo;
|
}
|
||||||
Dni = dni;
|
|
||||||
Asunto = asunto;
|
if (IndicaDenunciaAnonima(Tipo_Denuncia))
|
||||||
A_Quien_Denuncia = aQuienDenuncia;
|
{
|
||||||
Descripcion_Denuncia = descripcionDenuncia;
|
return true;
|
||||||
Denunciado_Ante_Inst = denunciadoAnteInst;
|
}
|
||||||
Modalidad_Informacion = modalidadInformacion;
|
|
||||||
Lugar_Hechos = lugarHechos;
|
return string.IsNullOrWhiteSpace(DocumentoResuelto) &&
|
||||||
Fecha_Hechos = fechaHechos;
|
string.IsNullOrWhiteSpace(NombreResuelto) &&
|
||||||
Notificacion_Preferencia = notificacionPreferencia;
|
string.IsNullOrWhiteSpace(RazonSocialResuelta);
|
||||||
Notificacion_Electronica = notificacionElectronica;
|
|
||||||
Correo_Electronico = correoElectronico;
|
|
||||||
Notificacion_Sms = notificacionSms;
|
|
||||||
Condiciones = condiciones;
|
|
||||||
Comments = comments;
|
|
||||||
NombreDenuncia = nombreDenuncia;
|
|
||||||
EstadoDenuncia = estadoDenuncia;
|
|
||||||
ArchivoElegido = archivoElegido;
|
|
||||||
FechaSubidaAGestiona = fechaSubidaAGestiona;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string NombreDenuncianteMostrar =>
|
||||||
|
EsPersonaJuridica
|
||||||
|
? RazonSocialResuelta
|
||||||
|
: string.Join(
|
||||||
|
' ',
|
||||||
|
new[] { NombreResuelto, PrimerApellidoResuelto, SegundoApellidoResuelto }
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string ExpedienteGestionaMostrable
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(CodigoExpedienteGestiona))
|
||||||
|
{
|
||||||
|
return CodigoExpedienteGestiona.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Expediente_Gestiona))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Equals(Expediente_Gestiona, "Pendiente", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? "Pendiente"
|
||||||
|
: "Pendiente de sincronizar";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<ReportFieldEntry> GetCamposFormulario()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(CamposFormularioJson))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<List<ReportFieldEntry>>(CamposFormularioJson) ?? [];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCamposFormulario(IEnumerable<ReportFieldEntry> fields)
|
||||||
|
{
|
||||||
|
var list = fields?.ToList() ?? [];
|
||||||
|
CamposFormularioJson = list.Count == 0
|
||||||
|
? string.Empty
|
||||||
|
: JsonSerializer.Serialize(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IndicaDenunciaAnonima(string? tipoDenuncia)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeLabel(tipoDenuncia);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IndicaDenunciaIdentificada(tipoDenuncia))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized == "anonima" ||
|
||||||
|
normalized == "anonimo" ||
|
||||||
|
normalized.Contains("denuncia anonima", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains("anonima", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains("anonimo", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IndicaDenunciaIdentificada(string? tipoDenuncia)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeLabel(tipoDenuncia);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized.Contains("no anonima", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains("no anonimo", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains("deseo identificarme", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains("quiero identificarme", StringComparison.Ordinal) ||
|
||||||
|
normalized.Contains("identificarme", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveValue(string currentValue, params string[] fallbackLabels)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(currentValue))
|
||||||
|
{
|
||||||
|
return currentValue.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var labels = fallbackLabels
|
||||||
|
.Select(NormalizeLabel)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var field in GetCamposFormulario())
|
||||||
|
{
|
||||||
|
if (labels.Contains(NormalizeLabel(field.Label)) &&
|
||||||
|
!string.IsNullOrWhiteSpace(field.Value))
|
||||||
|
{
|
||||||
|
return field.Value.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeLabel(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = value.Normalize(NormalizationForm.FormD);
|
||||||
|
var builder = new StringBuilder(normalized.Length);
|
||||||
|
|
||||||
|
foreach (var ch in normalized)
|
||||||
|
{
|
||||||
|
if (CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark)
|
||||||
|
{
|
||||||
|
builder.Append(char.ToLowerInvariant(ch));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString().Normalize(NormalizationForm.FormC).Trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models
|
||||||
|
{
|
||||||
|
public class ExpedienteTerceroDto
|
||||||
|
{
|
||||||
|
public string FileUrl { get; set; } = "";
|
||||||
|
public string? CodigoExpediente { get; set; } // name / code / etc.
|
||||||
|
public string? Asunto { get; set; } // subject / free_title
|
||||||
|
public DateTimeOffset? FechaCreacion { get; set; }
|
||||||
|
public string? Estado { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
// Models/FicherosDenuncias.cs
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace GestionaDenunciasAN.Models
|
namespace GestionaDenunciasAN.Models
|
||||||
{
|
{
|
||||||
@@ -13,7 +14,7 @@ namespace GestionaDenunciasAN.Models
|
|||||||
// Descripción del fichero (puede ser nula)
|
// Descripción del fichero (puede ser nula)
|
||||||
public string? Descripcion { get; set; }
|
public string? Descripcion { get; set; }
|
||||||
|
|
||||||
// Fecha de creación del fichero
|
// Fecha de creación del fichero original
|
||||||
public DateTime Fecha { get; set; }
|
public DateTime Fecha { get; set; }
|
||||||
|
|
||||||
// Observaciones
|
// Observaciones
|
||||||
@@ -28,12 +29,20 @@ namespace GestionaDenunciasAN.Models
|
|||||||
// Fichero completo en formato byte array (BLOB)
|
// Fichero completo en formato byte array (BLOB)
|
||||||
public byte[] Fichero { get; set; }
|
public byte[] Fichero { get; set; }
|
||||||
|
|
||||||
// Constructor por defecto
|
// → Nuevo: marca si ya se subió a Gestión
|
||||||
public FicherosDenuncias()
|
public bool Subido { get; set; }
|
||||||
{
|
|
||||||
}
|
// → 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 FicherosDenuncias() { }
|
||||||
|
|
||||||
// Constructor parametrizado que inicializa todos los campos
|
|
||||||
public FicherosDenuncias(
|
public FicherosDenuncias(
|
||||||
int id_Fichero,
|
int id_Fichero,
|
||||||
int id_Tipo,
|
int id_Tipo,
|
||||||
@@ -42,7 +51,8 @@ namespace GestionaDenunciasAN.Models
|
|||||||
string observaciones,
|
string observaciones,
|
||||||
int id_Denuncia,
|
int id_Denuncia,
|
||||||
string nombreFichero,
|
string nombreFichero,
|
||||||
byte[] fichero)
|
byte[] fichero
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Id_Fichero = id_Fichero;
|
Id_Fichero = id_Fichero;
|
||||||
Id_Tipo = id_Tipo;
|
Id_Tipo = id_Tipo;
|
||||||
@@ -52,6 +62,8 @@ namespace GestionaDenunciasAN.Models
|
|||||||
Id_Denuncia = id_Denuncia;
|
Id_Denuncia = id_Denuncia;
|
||||||
NombreFichero = nombreFichero;
|
NombreFichero = nombreFichero;
|
||||||
Fichero = fichero;
|
Fichero = fichero;
|
||||||
|
Subido = false;
|
||||||
|
FechaSubida = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed record FileDownloadResult(byte[] Content, string FileName);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed class GestionaExpedienteInfo
|
||||||
|
{
|
||||||
|
public string FileUrl { get; set; } = string.Empty;
|
||||||
|
public string? CodigoExpediente { get; set; }
|
||||||
|
public string? FreeTitle { get; set; }
|
||||||
|
}
|
||||||
11
Antifraude.Net/GestionaDenunciasAN/Models/GestionaOptions.cs
Normal file
11
Antifraude.Net/GestionaDenunciasAN/Models/GestionaOptions.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models
|
||||||
|
{
|
||||||
|
public class GestionaOptions
|
||||||
|
{
|
||||||
|
public string ApiBase { get; set; } = null!;
|
||||||
|
public string AccessToken { get; set; } = null!;
|
||||||
|
public string UserLink { get; set; } = null!;
|
||||||
|
public string GroupLink { get; set; } = null!;
|
||||||
|
public string Location { get; set; } = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Antifraude.Net/GestionaDenunciasAN/Models/GlSession.cs
Normal file
3
Antifraude.Net/GestionaDenunciasAN/Models/GlSession.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed record GlSession(string Id, string Username, string? Role = null);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed class GlobalLeaksStoredSession
|
||||||
|
{
|
||||||
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
public string? SessionId { get; set; }
|
||||||
|
public string? Role { get; set; }
|
||||||
|
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
public bool HasActiveSession => !string.IsNullOrWhiteSpace(SessionId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed record ImportSummary(
|
||||||
|
int TotalCandidates,
|
||||||
|
int ImportedCount,
|
||||||
|
IReadOnlyList<string> Errors,
|
||||||
|
IReadOnlyList<int>? ImportedComplaintIds = null);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed record InboxUserState
|
||||||
|
{
|
||||||
|
public string Username { get; init; } = string.Empty;
|
||||||
|
public DateTimeOffset? LastSuccessfulDownloadAtUtc { get; init; }
|
||||||
|
public DateTimeOffset? LastDownloadedReportMomentUtc { get; init; }
|
||||||
|
public bool HasPreviousDownloads => LastDownloadedReportMomentUtc is not null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed record LoginRequest(string Username, string Password, string Authcode);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed record LoginResponse(string Username);
|
||||||
25
Antifraude.Net/GestionaDenunciasAN/Models/ReportDto.cs
Normal file
25
Antifraude.Net/GestionaDenunciasAN/Models/ReportDto.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed record ReportDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public int? Progressive { get; init; }
|
||||||
|
public string? ContextId { get; init; }
|
||||||
|
public string? ContextName { get; init; }
|
||||||
|
public string? CreationDate { get; init; }
|
||||||
|
public string? UpdateDate { get; init; }
|
||||||
|
public string? ExpirationDate { get; init; }
|
||||||
|
public string? ReminderDate { get; init; }
|
||||||
|
public string? AccessDate { get; init; }
|
||||||
|
public string? LastAccess { get; init; }
|
||||||
|
public string? Status { get; init; }
|
||||||
|
public bool Updated { get; init; }
|
||||||
|
public string? Label { get; init; }
|
||||||
|
public bool DownloadedByCurrentUser { get; init; }
|
||||||
|
public bool DownloadedByAnotherUser { get; init; }
|
||||||
|
public string? LastDownloadedByUsername { get; init; }
|
||||||
|
public string? LastDownloadedAt { get; init; }
|
||||||
|
public bool AlreadyImported { get; init; }
|
||||||
|
public bool AlreadyInGestiona { get; init; }
|
||||||
|
public string? TrackingNote { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed class ReportFieldEntry
|
||||||
|
{
|
||||||
|
public int Order { get; set; }
|
||||||
|
public string Section { get; set; } = string.Empty;
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed record ReportSummaryDto
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public int? Progressive { get; init; }
|
||||||
|
public string? CreationDate { get; init; }
|
||||||
|
public string? UpdateDate { get; init; }
|
||||||
|
public string? LastAccess { get; init; }
|
||||||
|
public string? AccessDate { get; init; }
|
||||||
|
public int? DaysSinceCreation { get; init; }
|
||||||
|
public int? DaysSinceAccess { get; init; }
|
||||||
|
public string? Status { get; init; }
|
||||||
|
public string? Label { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed class ThirdPartyAddressData
|
||||||
|
{
|
||||||
|
public string? Street { get; set; }
|
||||||
|
public string? Number { get; set; }
|
||||||
|
public string? Floor { get; set; }
|
||||||
|
public string? Door { get; set; }
|
||||||
|
public string? Block { get; set; }
|
||||||
|
public string? Stair { get; set; }
|
||||||
|
public string? Municipality { get; set; }
|
||||||
|
public string? Province { get; set; }
|
||||||
|
public string? ZipCode { get; set; }
|
||||||
|
public string? CountryCode { get; set; }
|
||||||
|
public string? RoadTypeCode { get; set; } = "CL";
|
||||||
|
|
||||||
|
public bool HasAnyValue =>
|
||||||
|
!string.IsNullOrWhiteSpace(Street) ||
|
||||||
|
!string.IsNullOrWhiteSpace(Number) ||
|
||||||
|
!string.IsNullOrWhiteSpace(Municipality) ||
|
||||||
|
!string.IsNullOrWhiteSpace(Province) ||
|
||||||
|
!string.IsNullOrWhiteSpace(ZipCode);
|
||||||
|
|
||||||
|
public static ThirdPartyAddressData? FromComplaint(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
var data = new ThirdPartyAddressData
|
||||||
|
{
|
||||||
|
Street = denuncia.Direccion,
|
||||||
|
RoadTypeCode = denuncia.DireccionTipoVia,
|
||||||
|
Number = denuncia.DireccionNumero,
|
||||||
|
Floor = denuncia.DireccionPiso,
|
||||||
|
Door = denuncia.DireccionPuerta,
|
||||||
|
Block = denuncia.DireccionBloque,
|
||||||
|
Stair = denuncia.DireccionEscalera,
|
||||||
|
Municipality = denuncia.Municipio,
|
||||||
|
Province = denuncia.Provincia,
|
||||||
|
ZipCode = denuncia.CodigoPostal,
|
||||||
|
CountryCode = string.IsNullOrWhiteSpace(denuncia.Pais) ? denuncia.PaisOrigen : denuncia.Pais,
|
||||||
|
};
|
||||||
|
|
||||||
|
return data.HasAnyValue ? data : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
public sealed class ThirdPartyIdentityData
|
||||||
|
{
|
||||||
|
public bool IsAnonymous { get; set; }
|
||||||
|
public bool IsLegalEntity { get; set; }
|
||||||
|
public string DocumentId { get; set; } = string.Empty;
|
||||||
|
public string FirstName { get; set; } = string.Empty;
|
||||||
|
public string LastName { get; set; } = string.Empty;
|
||||||
|
public string BusinessName { get; set; } = string.Empty;
|
||||||
|
public string Email { get; set; } = string.Empty;
|
||||||
|
public string CountryCode { get; set; } = string.Empty;
|
||||||
|
public ThirdPartyAddressData? Address { get; set; }
|
||||||
|
|
||||||
|
public string DisplayName =>
|
||||||
|
IsLegalEntity
|
||||||
|
? BusinessName
|
||||||
|
: string.Join(
|
||||||
|
' ',
|
||||||
|
new[] { FirstName, LastName }
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value)));
|
||||||
|
|
||||||
|
public static ThirdPartyIdentityData FromComplaint(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(denuncia);
|
||||||
|
|
||||||
|
var isAnonymous = denuncia.EsAnonima ||
|
||||||
|
string.Equals(
|
||||||
|
denuncia.DocumentoResuelto?.Trim(),
|
||||||
|
"00000000T",
|
||||||
|
StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return new ThirdPartyIdentityData
|
||||||
|
{
|
||||||
|
IsAnonymous = isAnonymous,
|
||||||
|
IsLegalEntity = !isAnonymous && denuncia.EsPersonaJuridica,
|
||||||
|
DocumentId = isAnonymous
|
||||||
|
? "00000000T"
|
||||||
|
: denuncia.DocumentoResuelto.Trim().ToUpperInvariant(),
|
||||||
|
FirstName = isAnonymous ? "Anonimo" : denuncia.NombreResuelto.Trim(),
|
||||||
|
LastName = isAnonymous ? "-" : BuildLastName(denuncia),
|
||||||
|
BusinessName = isAnonymous ? string.Empty : denuncia.RazonSocialResuelta.Trim(),
|
||||||
|
Email = (denuncia.Correo_Electronico ?? string.Empty).Trim(),
|
||||||
|
CountryCode = string.IsNullOrWhiteSpace(denuncia.Pais) ? denuncia.PaisOrigen : denuncia.Pais,
|
||||||
|
Address = isAnonymous ? null : ThirdPartyAddressData.FromComplaint(denuncia)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildLastName(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
var separated = string.Join(
|
||||||
|
' ',
|
||||||
|
new[] { denuncia.PrimerApellidoResuelto, denuncia.SegundoApellidoResuelto }
|
||||||
|
.Where(value => !string.IsNullOrWhiteSpace(value))
|
||||||
|
.Select(value => value.Trim()));
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(separated)
|
||||||
|
? denuncia.ApellidosResueltos.Trim()
|
||||||
|
: separated;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
public class UserState
|
public class UserState
|
||||||
{
|
{
|
||||||
private readonly object _lock = new object();
|
private readonly object _lock = new object();
|
||||||
private string _token;
|
private string _token = string.Empty;
|
||||||
private string _NombreUsu;
|
private string _NombreUsu = string.Empty;
|
||||||
private bool _Mostrar;
|
private bool _Mostrar;
|
||||||
|
|
||||||
public string Token
|
public string Token
|
||||||
|
|||||||
@@ -1,54 +1,64 @@
|
|||||||
|
using System.Globalization;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using System.Security.Claims;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using GestionaDenunciasAN.Components;
|
using GestionaDenunciasAN.Components;
|
||||||
|
using GestionaDenunciasAN.Configuration;
|
||||||
using GestionaDenunciasAN.Models;
|
using GestionaDenunciasAN.Models;
|
||||||
|
using GestionaDenunciasAN.Services;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.GetCultureInfo("es-ES");
|
||||||
|
CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo("es-ES");
|
||||||
|
|
||||||
|
builder.Services.Configure<GestionaOptions>(builder.Configuration.GetSection("Gestiona"));
|
||||||
|
builder.Services.Configure<GlobalLeaksOptions>(builder.Configuration.GetSection(GlobalLeaksOptions.SectionName));
|
||||||
|
builder.Services.Configure<ComplaintStorageOptions>(builder.Configuration.GetSection(ComplaintStorageOptions.SectionName));
|
||||||
|
|
||||||
// Configurar servicios
|
|
||||||
builder.Services.AddRazorComponents()
|
builder.Services.AddRazorComponents()
|
||||||
.AddInteractiveServerComponents();
|
.AddInteractiveServerComponents();
|
||||||
builder.Services.AddHttpClient("DefaultClient", client =>
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
{
|
|
||||||
client.BaseAddress = new Uri(Utilidades.urlSwagger());
|
builder.Services
|
||||||
});
|
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
builder.Services.AddAuthentication(options =>
|
|
||||||
{
|
|
||||||
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
|
||||||
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
|
||||||
})
|
|
||||||
.AddCookie(options =>
|
.AddCookie(options =>
|
||||||
{
|
{
|
||||||
options.LoginPath = "/home";
|
options.Cookie.Name = "denuncias.auth";
|
||||||
options.AccessDeniedPath = "/AccessDenied";
|
options.Cookie.HttpOnly = true;
|
||||||
|
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||||
|
options.LoginPath = "/";
|
||||||
|
options.LogoutPath = "/api/auth/logout";
|
||||||
|
options.ExpireTimeSpan = TimeSpan.FromHours(8);
|
||||||
|
options.SlidingExpiration = false;
|
||||||
});
|
});
|
||||||
// Necesario para ver porqu<71> est<73> fallando ciertas cosas que dan el error Circuit
|
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
builder.Services.AddDataProtection();
|
||||||
builder.Services.AddServerSideBlazor().AddCircuitOptions(option => { option.DetailedErrors = true; });
|
builder.Services.AddServerSideBlazor().AddCircuitOptions(option => { option.DetailedErrors = true; });
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddDistributedMemoryCache();
|
|
||||||
builder.Services.AddSession(options =>
|
|
||||||
{
|
|
||||||
options.IdleTimeout = TimeSpan.FromHours(1);
|
|
||||||
options.Cookie.HttpOnly = true;
|
|
||||||
options.Cookie.IsEssential = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddHttpClient("CertClient").ConfigurePrimaryHttpMessageHandler(() =>
|
|
||||||
{
|
|
||||||
return new HttpClientHandler
|
|
||||||
{
|
|
||||||
ClientCertificateOptions = ClientCertificateOption.Manual // Forzar la selecci<63>n del certificado
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
builder.Services.AddBlazorBootstrap();
|
builder.Services.AddBlazorBootstrap();
|
||||||
builder.Services.AddAntiforgery();
|
builder.Services.AddAntiforgery();
|
||||||
builder.Services.AddSingleton<UserState>();
|
builder.Services.AddScoped<UserState>();
|
||||||
|
builder.Services.AddSingleton<AppSessionLifetime>();
|
||||||
|
builder.Services.AddSingleton<LoginRateLimiter>();
|
||||||
|
builder.Services.AddSingleton<GlobalLeaksSessionStore>();
|
||||||
|
builder.Services.AddScoped<GlobalLeaksClient>();
|
||||||
|
builder.Services.AddScoped<IDenunciaStore, MySqlDenunciaStore>();
|
||||||
|
builder.Services.AddScoped<IInboxTrackingService, InboxTrackingService>();
|
||||||
|
builder.Services.AddScoped<DenunciaInboxService>();
|
||||||
|
builder.Services.AddScoped<GestionaDocumentWorkflowService>();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<IGestionaService, GestionaService>((sp, client) =>
|
||||||
|
{
|
||||||
|
var opts = sp.GetRequiredService<IOptions<GestionaOptions>>().Value;
|
||||||
|
client.BaseAddress = new Uri(opts.ApiBase);
|
||||||
|
client.DefaultRequestHeaders.Add("X-Gestiona-Access-Token", opts.AccessToken);
|
||||||
|
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@@ -58,48 +68,189 @@ if (!app.Environment.IsDevelopment())
|
|||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
app.UseStaticFiles();
|
|
||||||
app.UseSession();
|
|
||||||
app.UseRouting();
|
|
||||||
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
|
|
||||||
app.UseAntiforgery();
|
|
||||||
|
|
||||||
app.Use(async (context, next) =>
|
app.Use(async (context, next) =>
|
||||||
{
|
{
|
||||||
var userState = context.RequestServices.GetService<UserState>();
|
context.Response.Headers.XFrameOptions = "DENY";
|
||||||
var path = context.Request.Path;
|
context.Response.Headers.XContentTypeOptions = "nosniff";
|
||||||
|
context.Response.Headers["Referrer-Policy"] = "no-referrer";
|
||||||
// Permitir solicitudes internas y recursos necesarios
|
context.Response.Headers.ContentSecurityPolicy =
|
||||||
if (path == "/" ||
|
"default-src 'self'; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data:; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net";
|
||||||
path.StartsWithSegments("/_blazor") ||
|
|
||||||
path.StartsWithSegments("/Content") ||
|
|
||||||
path.StartsWithSegments("/Scripts") ||
|
|
||||||
path.StartsWithSegments("/js") ||
|
|
||||||
path.StartsWithSegments("/favicon.ico") ||
|
|
||||||
path.StartsWithSegments("/_framework"))
|
|
||||||
{
|
|
||||||
await next();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirigir al home si no hay token y la ruta no es p<>blica
|
|
||||||
if (userState?.Token == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Redirigiendo al home desde: {path}");
|
|
||||||
context.Response.Redirect("/");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continuar con la solicitud
|
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
var path = context.Request.Path;
|
||||||
|
var isPublic =
|
||||||
|
path == "/" ||
|
||||||
|
path.StartsWithSegments("/api") ||
|
||||||
|
path.StartsWithSegments("/_blazor") ||
|
||||||
|
path.StartsWithSegments("/_framework") ||
|
||||||
|
path.StartsWithSegments("/bootstrap") ||
|
||||||
|
path.StartsWithSegments("/Content") ||
|
||||||
|
path.StartsWithSegments("/Scripts") ||
|
||||||
|
path.StartsWithSegments("/css") ||
|
||||||
|
path.StartsWithSegments("/js") ||
|
||||||
|
path.StartsWithSegments("/favicon") ||
|
||||||
|
Path.HasExtension(path.Value);
|
||||||
|
|
||||||
|
if (context.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
var username = context.User.Identity?.Name?.Trim();
|
||||||
|
var appSessionLifetime = context.RequestServices.GetRequiredService<AppSessionLifetime>();
|
||||||
|
var cookieStartupStamp = context.User.FindFirst("app_startup_stamp")?.Value;
|
||||||
|
var hasStoredCredentials = false;
|
||||||
|
var cookieBelongsToCurrentStartup =
|
||||||
|
!string.IsNullOrWhiteSpace(cookieStartupStamp) &&
|
||||||
|
string.Equals(cookieStartupStamp, appSessionLifetime.StartupStamp, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(username))
|
||||||
|
{
|
||||||
|
var sessionStore = context.RequestServices.GetRequiredService<GlobalLeaksSessionStore>();
|
||||||
|
var storedSession = await sessionStore.GetAsync(username, context.RequestAborted);
|
||||||
|
hasStoredCredentials =
|
||||||
|
storedSession is not null &&
|
||||||
|
!string.IsNullOrWhiteSpace(storedSession.Username) &&
|
||||||
|
!string.IsNullOrWhiteSpace(storedSession.Password);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasStoredCredentials || !cookieBelongsToCurrentStartup)
|
||||||
|
{
|
||||||
|
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
|
||||||
|
if (!isPublic)
|
||||||
|
{
|
||||||
|
var returnUrl = $"{context.Request.Path}{context.Request.QueryString}";
|
||||||
|
context.Response.Redirect($"/?returnUrl={Uri.EscapeDataString(returnUrl)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPublic || context.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginReturnUrl = $"{context.Request.Path}{context.Request.QueryString}";
|
||||||
|
context.Response.Redirect($"/?returnUrl={Uri.EscapeDataString(loginReturnUrl)}");
|
||||||
|
});
|
||||||
|
app.UseAntiforgery();
|
||||||
|
|
||||||
|
var api = app.MapGroup("/api");
|
||||||
|
|
||||||
|
api.MapPost("/auth/login", async (
|
||||||
|
LoginRequest request,
|
||||||
|
HttpContext httpContext,
|
||||||
|
GlobalLeaksClient globalLeaksClient,
|
||||||
|
GlobalLeaksSessionStore sessionStore,
|
||||||
|
LoginRateLimiter rateLimiter,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||||
|
var appSessionLifetime = httpContext.RequestServices.GetRequiredService<AppSessionLifetime>();
|
||||||
|
if (!rateLimiter.AllowAttempt(ip))
|
||||||
|
{
|
||||||
|
return Results.Json(
|
||||||
|
new ApiError("Demasiados intentos. Espera un minuto."),
|
||||||
|
statusCode: StatusCodes.Status429TooManyRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(request.Username) ||
|
||||||
|
string.IsNullOrWhiteSpace(request.Password) ||
|
||||||
|
string.IsNullOrWhiteSpace(request.Authcode))
|
||||||
|
{
|
||||||
|
return Results.Json(
|
||||||
|
new ApiError("Debes indicar usuario, contraseña y código 2FA."),
|
||||||
|
statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Regex.IsMatch(request.Authcode.Trim(), @"^\d{6}$"))
|
||||||
|
{
|
||||||
|
return Results.Json(
|
||||||
|
new ApiError("El código 2FA debe tener exactamente 6 dígitos."),
|
||||||
|
statusCode: StatusCodes.Status400BadRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var session = await globalLeaksClient.LoginAsync(
|
||||||
|
request.Username.Trim(),
|
||||||
|
request.Password,
|
||||||
|
request.Authcode.Trim(),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var resolvedUsername = string.IsNullOrWhiteSpace(session.Username)
|
||||||
|
? request.Username.Trim()
|
||||||
|
: session.Username.Trim();
|
||||||
|
|
||||||
|
session = new GlSession(session.Id, resolvedUsername, session.Role);
|
||||||
|
|
||||||
|
await sessionStore.SaveAsync(
|
||||||
|
session.Username,
|
||||||
|
request.Password,
|
||||||
|
session.Id,
|
||||||
|
session.Role,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(ClaimTypes.Name, session.Username),
|
||||||
|
new("app_startup_stamp", appSessionLifetime.StartupStamp),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(session.Role))
|
||||||
|
{
|
||||||
|
claims.Add(new Claim("gl_role", session.Role));
|
||||||
|
}
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
var authProperties = new AuthenticationProperties
|
||||||
|
{
|
||||||
|
IsPersistent = false,
|
||||||
|
AllowRefresh = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await httpContext.SignInAsync(
|
||||||
|
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||||
|
principal,
|
||||||
|
authProperties);
|
||||||
|
|
||||||
|
return Results.Ok(new LoginResponse(session.Username));
|
||||||
|
}
|
||||||
|
catch (GlobalLeaksValidationException ex)
|
||||||
|
{
|
||||||
|
return Results.Json(new ApiError(ex.Message), statusCode: ex.StatusCode);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Results.Json(
|
||||||
|
new ApiError("No se ha podido conectar con GlobalLeaks."),
|
||||||
|
statusCode: StatusCodes.Status502BadGateway);
|
||||||
|
}
|
||||||
|
}).DisableAntiforgery();
|
||||||
|
|
||||||
|
api.MapPost("/auth/logout", async (
|
||||||
|
HttpContext httpContext,
|
||||||
|
GlobalLeaksSessionStore sessionStore,
|
||||||
|
CancellationToken cancellationToken) =>
|
||||||
|
{
|
||||||
|
var username = httpContext.User.Identity?.Name;
|
||||||
|
if (!string.IsNullOrWhiteSpace(username))
|
||||||
|
{
|
||||||
|
await sessionStore.DeleteAsync(username, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
return Results.Ok(new { ok = true });
|
||||||
|
}).DisableAntiforgery();
|
||||||
|
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
.AddInteractiveServerRenderMode();
|
.AddInteractiveServerRenderMode();
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS complaints (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
external_registry_id INT NOT NULL DEFAULT 0,
|
||||||
|
external_report_id INT NOT NULL,
|
||||||
|
report_date_utc DATETIME(6) NULL,
|
||||||
|
gestiona_file_url VARCHAR(768) NOT NULL DEFAULT '',
|
||||||
|
gestiona_file_code VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
gestiona_person_id INT NOT NULL DEFAULT 0,
|
||||||
|
tag VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
status_name VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
complaint_type VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
reporter_kind VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
is_legal_entity TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
reporter_first_name VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
reporter_first_surname VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
reporter_second_surname VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
reporter_last_name VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
reporter_business_name VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
reporter_gender VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
reporter_document_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
reporter_document_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
reporter_origin_country VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
subject VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
accused_party VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
accused_party_details VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
complaint_description LONGTEXT NULL,
|
||||||
|
reported_to_institution VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
reported_institution_details VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
requested_protection VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
requested_protection_details VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
information_mode VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
facts_location VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
facts_date_utc DATETIME(6) NULL,
|
||||||
|
forwarding_authorization VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
forwarding_personal_data_preference VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
notification_preference VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
electronic_notification VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
online_tracking_preference VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
postal_notification_preference VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
email VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
sms_notification VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
accepted_terms TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
comments LONGTEXT NULL,
|
||||||
|
phone VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
address_line VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
address_road_type VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
address_number VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
address_floor VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
address_door VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
address_block VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
address_stair VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
address_extra VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
municipality VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
province VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
postal_code VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
country_code VARCHAR(32) NOT NULL DEFAULT '',
|
||||||
|
form_fields_json LONGTEXT NULL,
|
||||||
|
raw_report_text LONGTEXT NULL,
|
||||||
|
is_confidential TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
is_update TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
gestiona_procedure_id CHAR(36) NULL,
|
||||||
|
gestiona_group_id CHAR(36) NULL,
|
||||||
|
display_name VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
workflow_status VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
selected_document_name TEXT NULL,
|
||||||
|
gestiona_uploaded_at_utc DATETIME(6) NULL,
|
||||||
|
is_in_gestiona TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
is_rejected TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
updated_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_complaints_external_report_id (external_report_id),
|
||||||
|
KEY ix_complaints_flags (is_update, is_in_gestiona, is_rejected),
|
||||||
|
KEY ix_complaints_uploaded_at (gestiona_uploaded_at_utc)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS app_users (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
username VARCHAR(256) NOT NULL,
|
||||||
|
last_successful_download_at_utc DATETIME(6) NULL,
|
||||||
|
last_downloaded_report_moment_utc DATETIME(6) NULL,
|
||||||
|
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
updated_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_app_users_username (username)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS inbox_reports (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
global_report_uuid CHAR(36) NOT NULL,
|
||||||
|
progressive_id INT NULL,
|
||||||
|
context_id VARCHAR(128) NULL,
|
||||||
|
context_name VARCHAR(256) NULL,
|
||||||
|
creation_date_utc DATETIME(6) NULL,
|
||||||
|
update_date_utc DATETIME(6) NULL,
|
||||||
|
access_date_utc DATETIME(6) NULL,
|
||||||
|
last_access_utc DATETIME(6) NULL,
|
||||||
|
gl_status VARCHAR(64) NULL,
|
||||||
|
gl_label VARCHAR(128) NULL,
|
||||||
|
is_updated TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
first_seen_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
last_seen_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||||
|
last_downloaded_at_utc DATETIME(6) NULL,
|
||||||
|
last_downloaded_by_user_id BIGINT NULL,
|
||||||
|
imported_complaint_report_id INT NULL,
|
||||||
|
imported_to_store_at_utc DATETIME(6) NULL,
|
||||||
|
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
updated_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_inbox_reports_uuid (global_report_uuid),
|
||||||
|
KEY ix_inbox_reports_progressive (progressive_id),
|
||||||
|
KEY ix_inbox_reports_downloaded (last_downloaded_at_utc),
|
||||||
|
CONSTRAINT fk_inbox_reports_last_user
|
||||||
|
FOREIGN KEY (last_downloaded_by_user_id) REFERENCES app_users(id)
|
||||||
|
ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_inbox_reports (
|
||||||
|
app_user_id BIGINT NOT NULL,
|
||||||
|
inbox_report_id BIGINT NOT NULL,
|
||||||
|
first_seen_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
last_seen_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||||
|
first_downloaded_at_utc DATETIME(6) NULL,
|
||||||
|
last_downloaded_at_utc DATETIME(6) NULL,
|
||||||
|
download_count INT NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY (app_user_id, inbox_report_id),
|
||||||
|
KEY ix_user_inbox_reports_last_downloaded (last_downloaded_at_utc),
|
||||||
|
CONSTRAINT fk_user_inbox_reports_user
|
||||||
|
FOREIGN KEY (app_user_id) REFERENCES app_users(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_user_inbox_reports_report
|
||||||
|
FOREIGN KEY (inbox_report_id) REFERENCES inbox_reports(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS complaint_attachments (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
complaint_id BIGINT NOT NULL,
|
||||||
|
attachment_type_id INT NOT NULL DEFAULT 0,
|
||||||
|
description VARCHAR(512) NULL,
|
||||||
|
attachment_date_utc DATETIME(6) NULL,
|
||||||
|
notes VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
|
original_file_name VARCHAR(512) NOT NULL,
|
||||||
|
content LONGBLOB NOT NULL,
|
||||||
|
content_mime_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
|
content_sha256 CHAR(64) NOT NULL,
|
||||||
|
uploaded_to_gestiona TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
uploaded_at_utc DATETIME(6) NULL,
|
||||||
|
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
updated_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uq_attachments_complaint_filename (complaint_id, original_file_name),
|
||||||
|
KEY ix_attachments_uploaded (uploaded_to_gestiona, uploaded_at_utc),
|
||||||
|
KEY ix_attachments_sha256 (content_sha256),
|
||||||
|
CONSTRAINT fk_attachments_complaint
|
||||||
|
FOREIGN KEY (complaint_id) REFERENCES complaints(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace GestionaDenunciasAN.Services;
|
||||||
|
|
||||||
|
public sealed class AppSessionLifetime
|
||||||
|
{
|
||||||
|
public string StartupStamp { get; } = Guid.NewGuid().ToString("N");
|
||||||
|
}
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
using GestionaDenunciasAN.Helpers;
|
||||||
|
using GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Services;
|
||||||
|
|
||||||
|
public sealed class DenunciaInboxService
|
||||||
|
{
|
||||||
|
private const string RootPath = @"C:\ZipsDenuncias";
|
||||||
|
|
||||||
|
private readonly IGestionaService _gestionaService;
|
||||||
|
private readonly IDenunciaStore _denunciaStore;
|
||||||
|
private readonly ILogger<DenunciaInboxService> _logger;
|
||||||
|
|
||||||
|
public DenunciaInboxService(
|
||||||
|
IGestionaService gestionaService,
|
||||||
|
IDenunciaStore denunciaStore,
|
||||||
|
ILogger<DenunciaInboxService> logger)
|
||||||
|
{
|
||||||
|
_gestionaService = gestionaService;
|
||||||
|
_denunciaStore = denunciaStore;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureStorageReadyAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(RootPath);
|
||||||
|
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<string>> GetExistingZipNamesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await EnsureStorageReadyAsync(cancellationToken);
|
||||||
|
return Directory
|
||||||
|
.GetFiles(RootPath, "*.zip")
|
||||||
|
.Select(Path.GetFileName)
|
||||||
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||||
|
.OrderBy(name => name, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteZipAsync(string zipName, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await EnsureStorageReadyAsync(cancellationToken);
|
||||||
|
|
||||||
|
var fullPath = Path.Combine(RootPath, zipName);
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
{
|
||||||
|
File.Delete(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ImportSummary> ProcessPendingFolderZipsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await EnsureStorageReadyAsync(cancellationToken);
|
||||||
|
|
||||||
|
var zipPaths = Directory.GetFiles(RootPath, "*.zip")
|
||||||
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var errors = new List<string>();
|
||||||
|
var importedCount = 0;
|
||||||
|
var complaintIds = new List<int>();
|
||||||
|
|
||||||
|
foreach (var zipPath in zipPaths)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var zipBytes = await File.ReadAllBytesAsync(zipPath, cancellationToken);
|
||||||
|
var complaintId = await ProcessZipAsync(zipBytes, Path.GetFileName(zipPath), null, cancellationToken);
|
||||||
|
File.Delete(zipPath);
|
||||||
|
importedCount++;
|
||||||
|
complaintIds.Add(complaintId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error procesando el ZIP local {ZipPath}", zipPath);
|
||||||
|
errors.Add($"{Path.GetFileName(zipPath)}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ImportSummary(zipPaths.Count, importedCount, errors, complaintIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ImportSummary> ImportFromGlobalLeaksAsync(
|
||||||
|
FileDownloadResult zipDownload,
|
||||||
|
FileDownloadResult? jsonDownload,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await EnsureStorageReadyAsync(cancellationToken);
|
||||||
|
|
||||||
|
var fileName = string.IsNullOrWhiteSpace(zipDownload.FileName)
|
||||||
|
? $"report-{Guid.NewGuid():N}.zip"
|
||||||
|
: zipDownload.FileName;
|
||||||
|
|
||||||
|
var json = jsonDownload is null
|
||||||
|
? null
|
||||||
|
: Encoding.UTF8.GetString(jsonDownload.Content);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var complaintId = await ProcessZipAsync(zipDownload.Content, fileName, json, cancellationToken);
|
||||||
|
return new ImportSummary(1, 1, [], [complaintId]);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error importando denuncia desde GlobalLeaks {FileName}", fileName);
|
||||||
|
return new ImportSummary(1, 0, [$"{fileName}: {ex.Message}"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<int> ProcessZipAsync(
|
||||||
|
byte[] zipBytes,
|
||||||
|
string sourceName,
|
||||||
|
string? globalLeaksJson,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
|
||||||
|
if (reportEntry is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("El ZIP no contiene el fichero report.txt.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var reportText = await ReadEntryTextAsync(reportEntry, cancellationToken);
|
||||||
|
var denuncia = ReportParser.ParseReport(reportText);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(globalLeaksJson))
|
||||||
|
{
|
||||||
|
GlobalLeaksJsonEnricher.Enrich(denuncia, globalLeaksJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (denuncia.Id_Denuncia == 0)
|
||||||
|
{
|
||||||
|
denuncia.Id_Denuncia = TryReadIdFromReport(reportText);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (denuncia.Id_Denuncia == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"No se ha podido determinar el identificador de la denuncia en {sourceName}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
denuncia.ProcedureId = Guid.Empty;
|
||||||
|
denuncia.GroupId = Guid.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(denuncia.Expediente_Gestiona))
|
||||||
|
{
|
||||||
|
denuncia.Expediente_Gestiona = "Pendiente";
|
||||||
|
}
|
||||||
|
|
||||||
|
var nuevosFicheros = await ReadFilesFromArchiveAsync(archive, reportEntry, denuncia.Id_Denuncia, cancellationToken);
|
||||||
|
await MergeComplaintAsync(denuncia, cancellationToken);
|
||||||
|
await MergeFilesAsync(nuevosFicheros, cancellationToken);
|
||||||
|
return denuncia.Id_Denuncia;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<FicherosDenuncias>> ReadFilesFromArchiveAsync(
|
||||||
|
ZipArchive archive,
|
||||||
|
ZipArchiveEntry reportEntry,
|
||||||
|
int denunciaId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var files = new List<FicherosDenuncias>
|
||||||
|
{
|
||||||
|
new(
|
||||||
|
id_Fichero: 0,
|
||||||
|
id_Tipo: 1,
|
||||||
|
descripcion: "report.txt original",
|
||||||
|
fecha: reportEntry.LastWriteTime.UtcDateTime == DateTime.MinValue
|
||||||
|
? DateTime.UtcNow
|
||||||
|
: reportEntry.LastWriteTime.UtcDateTime,
|
||||||
|
observaciones: "",
|
||||||
|
id_Denuncia: denunciaId,
|
||||||
|
nombreFichero: "report.txt",
|
||||||
|
fichero: await ReadEntryBytesAsync(reportEntry, cancellationToken))
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var entry in archive.Entries.Where(IsSupportedAttachmentEntry))
|
||||||
|
{
|
||||||
|
files.Add(new FicherosDenuncias(
|
||||||
|
id_Fichero: 0,
|
||||||
|
id_Tipo: 1,
|
||||||
|
descripcion: null,
|
||||||
|
fecha: entry.LastWriteTime.UtcDateTime == DateTime.MinValue
|
||||||
|
? DateTime.UtcNow
|
||||||
|
: entry.LastWriteTime.UtcDateTime,
|
||||||
|
observaciones: "",
|
||||||
|
id_Denuncia: denunciaId,
|
||||||
|
nombreFichero: Path.GetFileName(entry.FullName),
|
||||||
|
fichero: await ReadEntryBytesAsync(entry, cancellationToken)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MergeComplaintAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var existing = await _denunciaStore.GetDenunciaByIdAsync(denuncia.Id_Denuncia, cancellationToken);
|
||||||
|
|
||||||
|
if (existing is not null)
|
||||||
|
{
|
||||||
|
CopyComplaintData(existing, denuncia);
|
||||||
|
await CompleteGestionaStatusAsync(existing, existing, cancellationToken);
|
||||||
|
await _denunciaStore.UpsertDenunciaAsync(existing, cancellationToken);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CompleteGestionaStatusAsync(denuncia, null, cancellationToken);
|
||||||
|
await _denunciaStore.UpsertDenunciaAsync(denuncia, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task MergeFilesAsync(List<FicherosDenuncias> nuevosFicheros, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return _denunciaStore.UpsertFicherosAsync(nuevosFicheros, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CompleteGestionaStatusAsync(
|
||||||
|
DenunciasGestiona denuncia,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await CompleteGestionaStatusAsync(denuncia, null, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CompleteGestionaStatusAsync(
|
||||||
|
DenunciasGestiona denuncia,
|
||||||
|
DenunciasGestiona? storedComplaint,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (TryApplyGestionaStatusFromStore(denuncia, storedComplaint))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var match = await _gestionaService.BuscarExpedientePorIdEnAsuntoAsync(denuncia.Id_Denuncia);
|
||||||
|
if (match is null)
|
||||||
|
{
|
||||||
|
ApplyPendingGestionaStatus(denuncia);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
denuncia.EsActualizacion = true;
|
||||||
|
denuncia.EnGestiona = true;
|
||||||
|
denuncia.Expediente_Gestiona = match.FileUrl;
|
||||||
|
denuncia.CodigoExpedienteGestiona = match.CodigoExpediente ?? string.Empty;
|
||||||
|
denuncia.NombreDenuncia = match.FreeTitle ?? $"Denuncia {denuncia.Id_Denuncia}-CD";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ApplyPendingGestionaStatus(denuncia);
|
||||||
|
_logger.LogWarning(
|
||||||
|
ex,
|
||||||
|
"No se ha podido comprobar si la denuncia {DenunciaId} ya existia en Gestiona",
|
||||||
|
denuncia.Id_Denuncia);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryApplyGestionaStatusFromStore(
|
||||||
|
DenunciasGestiona target,
|
||||||
|
DenunciasGestiona? storedComplaint)
|
||||||
|
{
|
||||||
|
if (!IsAlreadyUploadedToGestiona(storedComplaint))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.EsActualizacion = true;
|
||||||
|
target.EnGestiona = true;
|
||||||
|
target.Expediente_Gestiona = string.IsNullOrWhiteSpace(storedComplaint!.Expediente_Gestiona)
|
||||||
|
? "Pendiente"
|
||||||
|
: storedComplaint.Expediente_Gestiona;
|
||||||
|
target.CodigoExpedienteGestiona = storedComplaint.CodigoExpedienteGestiona ?? string.Empty;
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(storedComplaint.NombreDenuncia))
|
||||||
|
{
|
||||||
|
target.NombreDenuncia = storedComplaint.NombreDenuncia;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storedComplaint.FechaSubidaAGestiona != DateTime.MinValue)
|
||||||
|
{
|
||||||
|
target.FechaSubidaAGestiona = storedComplaint.FechaSubidaAGestiona;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !string.IsNullOrWhiteSpace(target.CodigoExpedienteGestiona);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyPendingGestionaStatus(DenunciasGestiona denuncia)
|
||||||
|
{
|
||||||
|
denuncia.EsActualizacion = false;
|
||||||
|
denuncia.EnGestiona = false;
|
||||||
|
denuncia.Expediente_Gestiona = "Pendiente";
|
||||||
|
denuncia.CodigoExpedienteGestiona = string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(denuncia.NombreDenuncia))
|
||||||
|
{
|
||||||
|
denuncia.NombreDenuncia = $"Denuncia {denuncia.Id_Denuncia}-CD";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAlreadyUploadedToGestiona(DenunciasGestiona? denuncia)
|
||||||
|
{
|
||||||
|
if (denuncia is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (denuncia.EnGestiona || denuncia.FechaSubidaAGestiona != DateTime.MinValue)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !string.IsNullOrWhiteSpace(denuncia.Expediente_Gestiona) &&
|
||||||
|
!string.Equals(denuncia.Expediente_Gestiona, "Pendiente", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyComplaintData(DenunciasGestiona target, DenunciasGestiona source)
|
||||||
|
{
|
||||||
|
target.Fecha = source.Fecha;
|
||||||
|
target.Etiqueta = source.Etiqueta;
|
||||||
|
target.Estado = source.Estado;
|
||||||
|
target.Tipo_Denuncia = source.Tipo_Denuncia;
|
||||||
|
target.TipoDenunciante = source.TipoDenunciante;
|
||||||
|
target.EsPersonaJuridica = source.EsPersonaJuridica;
|
||||||
|
target.Confidencial = source.Confidencial;
|
||||||
|
target.Nombre = source.Nombre;
|
||||||
|
target.PrimerApellido = source.PrimerApellido;
|
||||||
|
target.SegundoApellido = source.SegundoApellido;
|
||||||
|
target.Apellidos = source.Apellidos;
|
||||||
|
target.RazonSocial = source.RazonSocial;
|
||||||
|
target.Sexo = source.Sexo;
|
||||||
|
target.Dni = source.Dni;
|
||||||
|
target.TipoDocumentoIdentificativo = source.TipoDocumentoIdentificativo;
|
||||||
|
target.PaisOrigen = source.PaisOrigen;
|
||||||
|
target.Asunto = source.Asunto;
|
||||||
|
target.A_Quien_Denuncia = source.A_Quien_Denuncia;
|
||||||
|
target.DenunciadoDetalle = source.DenunciadoDetalle;
|
||||||
|
target.Descripcion_Denuncia = source.Descripcion_Denuncia;
|
||||||
|
target.Denunciado_Ante_Inst = source.Denunciado_Ante_Inst;
|
||||||
|
target.OrganismoDenunciado = source.OrganismoDenunciado;
|
||||||
|
target.SolicitaProteccion = source.SolicitaProteccion;
|
||||||
|
target.MedidasProteccionSolicitadas = source.MedidasProteccionSolicitadas;
|
||||||
|
target.Modalidad_Informacion = source.Modalidad_Informacion;
|
||||||
|
target.Lugar_Hechos = source.Lugar_Hechos;
|
||||||
|
target.Fecha_Hechos = source.Fecha_Hechos;
|
||||||
|
target.AutorizaRemision = source.AutorizaRemision;
|
||||||
|
target.PreferenciaRemision = source.PreferenciaRemision;
|
||||||
|
target.Notificacion_Preferencia = source.Notificacion_Preferencia;
|
||||||
|
target.Notificacion_Electronica = source.Notificacion_Electronica;
|
||||||
|
target.SeguimientoOnline = source.SeguimientoOnline;
|
||||||
|
target.NotificacionPostal = source.NotificacionPostal;
|
||||||
|
target.Correo_Electronico = source.Correo_Electronico;
|
||||||
|
target.Notificacion_Sms = source.Notificacion_Sms;
|
||||||
|
target.Condiciones = source.Condiciones;
|
||||||
|
target.Comments = source.Comments;
|
||||||
|
target.Telefono = source.Telefono;
|
||||||
|
target.Direccion = source.Direccion;
|
||||||
|
target.DireccionTipoVia = source.DireccionTipoVia;
|
||||||
|
target.DireccionNumero = source.DireccionNumero;
|
||||||
|
target.DireccionPiso = source.DireccionPiso;
|
||||||
|
target.DireccionPuerta = source.DireccionPuerta;
|
||||||
|
target.DireccionBloque = source.DireccionBloque;
|
||||||
|
target.DireccionEscalera = source.DireccionEscalera;
|
||||||
|
target.DireccionExtra = source.DireccionExtra;
|
||||||
|
target.Municipio = source.Municipio;
|
||||||
|
target.Provincia = source.Provincia;
|
||||||
|
target.CodigoPostal = source.CodigoPostal;
|
||||||
|
target.Pais = source.Pais;
|
||||||
|
target.CamposFormularioJson = source.CamposFormularioJson;
|
||||||
|
target.TextoOriginalReport = source.TextoOriginalReport;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSupportedAttachmentEntry(ZipArchiveEntry entry)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(entry.Name))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = NormalizeEntryPath(entry.FullName);
|
||||||
|
return IsDirectChildOf(normalized, "files") || IsDirectChildOf(normalized, "files_attached_from_recipients");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsDirectChildOf(string normalizedEntryPath, string rootFolder)
|
||||||
|
{
|
||||||
|
if (!normalizedEntryPath.StartsWith(rootFolder + "/", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining = normalizedEntryPath[(rootFolder.Length + 1)..];
|
||||||
|
return !remaining.Contains('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeEntryPath(string path)
|
||||||
|
{
|
||||||
|
return path.Replace('\\', '/').Trim('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadEntryTextAsync(
|
||||||
|
ZipArchiveEntry entry,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var bytes = await ReadEntryBytesAsync(entry, cancellationToken);
|
||||||
|
return Encoding.UTF8.GetString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<byte[]> ReadEntryBytesAsync(
|
||||||
|
ZipArchiveEntry entry,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await using var entryStream = entry.Open();
|
||||||
|
using var memory = new MemoryStream();
|
||||||
|
await entryStream.CopyToAsync(memory, cancellationToken);
|
||||||
|
return memory.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int TryReadIdFromReport(string reportText)
|
||||||
|
{
|
||||||
|
using var reader = new StringReader(reportText);
|
||||||
|
string? line;
|
||||||
|
|
||||||
|
while ((line = reader.ReadLine()) is not null)
|
||||||
|
{
|
||||||
|
line = line.Trim();
|
||||||
|
if (line.StartsWith("ID:", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
int.TryParse(line[3..].Trim(), out var id))
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Services;
|
||||||
|
|
||||||
|
public sealed class GestionaDocumentWorkflowService
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ILogger<GestionaDocumentWorkflowService> _logger;
|
||||||
|
|
||||||
|
public GestionaDocumentWorkflowService(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<GestionaDocumentWorkflowService> logger)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GestionaApiBase =>
|
||||||
|
(_configuration["Gestiona:ApiBase"] ?? "https://02.g3stiona.com").TrimEnd('/');
|
||||||
|
|
||||||
|
private string GestionaAccessToken =>
|
||||||
|
_configuration["Gestiona:AccessToken"]
|
||||||
|
?? throw new InvalidOperationException("Falta Gestiona:AccessToken en appsettings.");
|
||||||
|
|
||||||
|
private string? PreferredCircuitTemplateName =>
|
||||||
|
_configuration["Gestiona:PreferredCircuitTemplateName"];
|
||||||
|
|
||||||
|
public async Task<string> UploadDocumentAndReturnUrlAsync(string fileUrl, byte[] contentBytes, string fileName)
|
||||||
|
{
|
||||||
|
var fileUrlAbs = EnsureAbsoluteGestionaUrl(fileUrl, GestionaApiBase);
|
||||||
|
var documentsTargetUrl = ResolveDocumentsContainerUrl(fileUrlAbs);
|
||||||
|
var uploadUri = await CreateUploadAsync(contentBytes, fileName);
|
||||||
|
|
||||||
|
var metaPayload = new
|
||||||
|
{
|
||||||
|
type = "DIGITAL",
|
||||||
|
name = fileName,
|
||||||
|
description = "Documento de denuncia",
|
||||||
|
elaboration_state = "EE01",
|
||||||
|
metadata_language = "ES",
|
||||||
|
links = new[] { new { rel = "content", href = uploadUri } }
|
||||||
|
};
|
||||||
|
|
||||||
|
var metaJson = JsonSerializer.Serialize(metaPayload);
|
||||||
|
|
||||||
|
using var metaReq = new HttpRequestMessage(HttpMethod.Post, documentsTargetUrl);
|
||||||
|
metaReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", GestionaAccessToken);
|
||||||
|
metaReq.Headers.Accept.Clear();
|
||||||
|
metaReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.gestiona.file-document+json")
|
||||||
|
{
|
||||||
|
Parameters = { new NameValueHeaderValue("version", "4") }
|
||||||
|
});
|
||||||
|
|
||||||
|
metaReq.Content = new StringContent(metaJson, Encoding.UTF8);
|
||||||
|
metaReq.Content.Headers.ContentType =
|
||||||
|
MediaTypeHeaderValue.Parse("application/vnd.gestiona.file-document+json; version=4");
|
||||||
|
|
||||||
|
using var metaResp = await CreateRawHttp().SendAsync(metaReq);
|
||||||
|
var body = await metaResp.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (!metaResp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"UploadDocumentAndReturnUrlAsync: {(int)metaResp.StatusCode} {metaResp.StatusCode}\n{body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var location = metaResp.Headers.Location?.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(location))
|
||||||
|
{
|
||||||
|
return location!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(body))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(body);
|
||||||
|
if (doc.RootElement.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Fallback a busqueda por nombre.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 json = payload.ToJsonString(new JsonSerializerOptions(JsonSerializerDefaults.Web));
|
||||||
|
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Post, $"{docUrlAbs.TrimEnd('/')}/circuit");
|
||||||
|
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", GestionaAccessToken);
|
||||||
|
req.Headers.TryAddWithoutValidation("Accept", "application/json");
|
||||||
|
req.Content = new StringContent(json, Encoding.UTF8);
|
||||||
|
req.Content.Headers.ContentType =
|
||||||
|
MediaTypeHeaderValue.Parse("application/vnd.gestiona.circuits.template-filedoc+json; version=7");
|
||||||
|
|
||||||
|
using var resp = await CreateRawHttp().SendAsync(req);
|
||||||
|
var body = await resp.Content.ReadAsStringAsync();
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
_logger.LogError(
|
||||||
|
"Fallo al tramitar documento {DocumentUrl} con plantilla {TemplateName} ({TemplateHref}). Status: {StatusCode}. Body: {Body}",
|
||||||
|
docUrlAbs,
|
||||||
|
template.Name ?? "(sin nombre)",
|
||||||
|
template.Href,
|
||||||
|
(int)resp.StatusCode,
|
||||||
|
body);
|
||||||
|
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"TramitarDocumentoAsync: {(int)resp.StatusCode} {resp.StatusCode}\n{body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Documento {DocumentUrl} enviado a circuito {TemplateName} ({TemplateHref}) para denuncia {ComplaintId}.",
|
||||||
|
docUrlAbs,
|
||||||
|
template.Name ?? "(sin nombre)",
|
||||||
|
template.Href,
|
||||||
|
complaintId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpClient CreateRawHttp() => _httpClientFactory.CreateClient();
|
||||||
|
|
||||||
|
private async Task<string> CreateUploadAsync(byte[] contentBytes, string fileName)
|
||||||
|
{
|
||||||
|
using var createReq = new HttpRequestMessage(HttpMethod.Post, $"{GestionaApiBase}/rest/uploads");
|
||||||
|
createReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", GestionaAccessToken);
|
||||||
|
createReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
|
||||||
|
using var createResp = await CreateRawHttp().SendAsync(createReq);
|
||||||
|
var createBody = await createResp.Content.ReadAsStringAsync();
|
||||||
|
if (!createResp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"CreateUploadAsync (POST): {(int)createResp.StatusCode} {createResp.StatusCode}\n{createBody}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadUri = createResp.Headers.Location?.ToString()
|
||||||
|
?? throw new InvalidOperationException("No se devolvio Location en /rest/uploads");
|
||||||
|
|
||||||
|
using var putReq = new HttpRequestMessage(HttpMethod.Put, uploadUri);
|
||||||
|
putReq.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", GestionaAccessToken);
|
||||||
|
putReq.Headers.TryAddWithoutValidation("X-Gestiona-Upload-MD5", GetMd5Hex(contentBytes));
|
||||||
|
putReq.Headers.TryAddWithoutValidation("Slug", fileName);
|
||||||
|
putReq.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
|
putReq.Content = new ByteArrayContent(contentBytes);
|
||||||
|
putReq.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
|
||||||
|
|
||||||
|
using var putResp = await CreateRawHttp().SendAsync(putReq);
|
||||||
|
var infoJson = await putResp.Content.ReadAsStringAsync();
|
||||||
|
if (!putResp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"CreateUploadAsync (PUT): {(int)putResp.StatusCode} {putResp.StatusCode}\n{infoJson}");
|
||||||
|
}
|
||||||
|
|
||||||
|
using var infoDoc = JsonDocument.Parse(infoJson);
|
||||||
|
var status = infoDoc.RootElement.TryGetProperty("status", out var pStatus) ? pStatus.GetString() : null;
|
||||||
|
if (!string.Equals(status, "READY", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Upload no READY: {status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return uploadUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string?> 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('/');
|
||||||
|
return normalized.Contains("/documents-and-folders", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? normalized
|
||||||
|
: $"{normalized}/documents-and-folders";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CircuitTemplateCandidate> ObtenerTemplateCircuitoFirmaAsync(string documentUrl)
|
||||||
|
{
|
||||||
|
using var req = new HttpRequestMessage(HttpMethod.Get, $"{documentUrl.TrimEnd('/')}/circuit/templates");
|
||||||
|
req.Headers.TryAddWithoutValidation("X-Gestiona-Access-Token", GestionaAccessToken);
|
||||||
|
req.Headers.TryAddWithoutValidation("Accept", "application/vnd.gestiona.circuits.templates-filedoc-page");
|
||||||
|
|
||||||
|
using var resp = await CreateRawHttp().SendAsync(req);
|
||||||
|
var body = await resp.Content.ReadAsStringAsync();
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"ObtenerTemplateCircuitoFirmaAsync: {(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)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("No se ha podido leer el listado de plantillas de circuito.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var templates = new List<CircuitTemplateCandidate>();
|
||||||
|
foreach (var item in content.EnumerateArray())
|
||||||
|
{
|
||||||
|
var name = item.TryGetProperty("name", out var pName) ? pName.GetString() : null;
|
||||||
|
var href = GetSelfHref(item);
|
||||||
|
if (string.IsNullOrWhiteSpace(href))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = JsonNode.Parse(item.GetRawText()) as JsonObject ?? new JsonObject();
|
||||||
|
templates.Add(new CircuitTemplateCandidate(
|
||||||
|
Name: name,
|
||||||
|
Href: href!,
|
||||||
|
Payload: payload,
|
||||||
|
SignersCount: CountArrayItems(payload, "signers"),
|
||||||
|
BlockEdit: GetBoolean(payload, "block_edit")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templates.Count == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("No hay plantillas de circuito disponibles para el documento.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Plantillas de circuito para {DocumentUrl}: {Templates}",
|
||||||
|
documentUrl,
|
||||||
|
string.Join(
|
||||||
|
" | ",
|
||||||
|
templates.Select(template =>
|
||||||
|
$"{template.Name ?? "(sin nombre)"} [firmantes={template.SignersCount}, candado={template.BlockEdit}]")));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(PreferredCircuitTemplateName))
|
||||||
|
{
|
||||||
|
var configuredExact = templates.FirstOrDefault(template =>
|
||||||
|
string.Equals(template.Name, PreferredCircuitTemplateName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (configuredExact is not null)
|
||||||
|
{
|
||||||
|
return configuredExact;
|
||||||
|
}
|
||||||
|
|
||||||
|
var configuredContains = templates.FirstOrDefault(template =>
|
||||||
|
!string.IsNullOrWhiteSpace(template.Name) &&
|
||||||
|
template.Name.Contains(PreferredCircuitTemplateName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (configuredContains is not null)
|
||||||
|
{
|
||||||
|
return configuredContains;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] preferredNames =
|
||||||
|
[
|
||||||
|
"CT-Actualizacion de denuncia",
|
||||||
|
"CT-Actualizaci\u00f3n de denuncia",
|
||||||
|
"Firma automatizada",
|
||||||
|
"Firma Sello de \u00D3rgano",
|
||||||
|
"Firma Sello de Organo"
|
||||||
|
];
|
||||||
|
|
||||||
|
var preferredTemplate = templates.FirstOrDefault(template =>
|
||||||
|
preferredNames.Any(preferred =>
|
||||||
|
string.Equals(template.Name, preferred, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
if (preferredTemplate is not null)
|
||||||
|
{
|
||||||
|
return preferredTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
var templatesWithSigners = templates
|
||||||
|
.Where(template => template.SignersCount > 0)
|
||||||
|
.OrderByDescending(template => template.BlockEdit)
|
||||||
|
.ThenByDescending(template => template.SignersCount)
|
||||||
|
.ToList();
|
||||||
|
if (templatesWithSigners.Count > 0)
|
||||||
|
{
|
||||||
|
return templatesWithSigners[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonObject BuildCircuitPayloadFromTemplate(
|
||||||
|
CircuitTemplateCandidate template,
|
||||||
|
string assignedGroupHref,
|
||||||
|
int? complaintId)
|
||||||
|
{
|
||||||
|
_ = assignedGroupHref;
|
||||||
|
_ = complaintId;
|
||||||
|
|
||||||
|
var payload = (JsonObject)template.Payload.DeepClone();
|
||||||
|
EnsureTemplateSelfLink(payload, template.Href);
|
||||||
|
payload.Remove("assigneds_can_use");
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureTemplateSelfLink(JsonObject payload, string templateSelfHref)
|
||||||
|
{
|
||||||
|
if (payload["links"] is not JsonArray links)
|
||||||
|
{
|
||||||
|
links = new JsonArray();
|
||||||
|
payload["links"] = links;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var node in links)
|
||||||
|
{
|
||||||
|
if (node is not JsonObject link)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rel = link["rel"]?.GetValue<string>();
|
||||||
|
var href = link["href"]?.GetValue<string>();
|
||||||
|
if (string.Equals(rel, "self", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!string.IsNullOrWhiteSpace(href))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
links.Add(JsonSerializer.SerializeToNode(new { rel = "self", href = templateSelfHref }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountArrayItems(JsonObject payload, string propertyName)
|
||||||
|
{
|
||||||
|
return payload[propertyName] is JsonArray array ? array.Count : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetBoolean(JsonObject payload, string propertyName)
|
||||||
|
{
|
||||||
|
return payload[propertyName]?.GetValue<bool?>() ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetSelfHref(JsonElement item)
|
||||||
|
{
|
||||||
|
if (!item.TryGetProperty("links", out var links) || links.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
return href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string EnsureAbsoluteGestionaUrl(string url, string apiBase)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("URL vacia.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Uri.TryCreate(url, UriKind.Absolute, out _))
|
||||||
|
{
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!url.StartsWith('/'))
|
||||||
|
{
|
||||||
|
url = "/" + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiBase.TrimEnd('/') + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetMd5Hex(byte[] bytes)
|
||||||
|
{
|
||||||
|
using var md5 = MD5.Create();
|
||||||
|
var hash = md5.ComputeHash(bytes);
|
||||||
|
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record CircuitTemplateCandidate(
|
||||||
|
string? Name,
|
||||||
|
string Href,
|
||||||
|
JsonObject Payload,
|
||||||
|
int SignersCount,
|
||||||
|
bool BlockEdit);
|
||||||
|
}
|
||||||
1104
Antifraude.Net/GestionaDenunciasAN/Services/GestionaService.cs
Normal file
1104
Antifraude.Net/GestionaDenunciasAN/Services/GestionaService.cs
Normal file
File diff suppressed because it is too large
Load Diff
662
Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksClient.cs
Normal file
662
Antifraude.Net/GestionaDenunciasAN/Services/GlobalLeaksClient.cs
Normal file
@@ -0,0 +1,662 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using GestionaDenunciasAN.Configuration;
|
||||||
|
using GestionaDenunciasAN.Models;
|
||||||
|
using Konscious.Security.Cryptography;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Services;
|
||||||
|
|
||||||
|
public sealed class GlobalLeaksClient
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<GlobalLeaksClient> _logger;
|
||||||
|
private readonly GlobalLeaksOptions _options;
|
||||||
|
|
||||||
|
public GlobalLeaksClient(IOptions<GlobalLeaksOptions> options, ILogger<GlobalLeaksClient> logger)
|
||||||
|
{
|
||||||
|
_options = options.Value;
|
||||||
|
_logger = logger;
|
||||||
|
_httpClient = new HttpClient
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri(_options.BaseUrl.TrimEnd('/')),
|
||||||
|
Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GlSession> LoginAsync(
|
||||||
|
string username,
|
||||||
|
string password,
|
||||||
|
string authcode,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var tokenRequest = CreateRequest(HttpMethod.Post, "/api/auth/token");
|
||||||
|
using var tokenResponse = await _httpClient.SendAsync(tokenRequest, cancellationToken);
|
||||||
|
await EnsureSuccessOrThrowAsync(tokenResponse, "/api/auth/token", cancellationToken);
|
||||||
|
var tokenData = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>(JsonOptions, cancellationToken)
|
||||||
|
?? throw new GlobalLeaksValidationException("No se pudo obtener el reto criptográfico.", 502);
|
||||||
|
|
||||||
|
var tokenAnswer = SolveProofOfWork(tokenData.Id, tokenData.Salt);
|
||||||
|
|
||||||
|
using var typeRequest = CreateRequest(HttpMethod.Post, "/api/auth/type");
|
||||||
|
typeRequest.Content = CreateJsonContent(new { username });
|
||||||
|
using var typeResponse = await _httpClient.SendAsync(typeRequest, cancellationToken);
|
||||||
|
await EnsureSuccessOrThrowAsync(typeResponse, "/api/auth/type", cancellationToken);
|
||||||
|
|
||||||
|
var authType = await typeResponse.Content.ReadFromJsonAsync<AuthTypeResponse>(JsonOptions, cancellationToken)
|
||||||
|
?? throw new GlobalLeaksValidationException("No se pudo obtener el tipo de autenticación.", 502);
|
||||||
|
|
||||||
|
var finalPassword = authType.Type == "key"
|
||||||
|
? DerivePassword(password, authType.Salt)
|
||||||
|
: password;
|
||||||
|
|
||||||
|
using var authRequest = CreateRequest(HttpMethod.Post, "/api/auth/authentication");
|
||||||
|
authRequest.Content = CreateJsonContent(new
|
||||||
|
{
|
||||||
|
tid = 1,
|
||||||
|
username,
|
||||||
|
password = finalPassword,
|
||||||
|
authcode,
|
||||||
|
});
|
||||||
|
authRequest.Headers.Add("X-Token", tokenAnswer);
|
||||||
|
|
||||||
|
using var authResponse = await _httpClient.SendAsync(authRequest, cancellationToken);
|
||||||
|
if (!authResponse.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var body = await ReadBodySafeAsync(authResponse, cancellationToken);
|
||||||
|
throw authResponse.StatusCode switch
|
||||||
|
{
|
||||||
|
HttpStatusCode.Unauthorized => new GlobalLeaksValidationException(
|
||||||
|
"Credenciales incorrectas o código 2FA inválido.",
|
||||||
|
StatusCodes.Status401Unauthorized),
|
||||||
|
(HttpStatusCode)429 => new GlobalLeaksValidationException(
|
||||||
|
"Demasiados intentos en GlobalLeaks. Espera unos minutos.",
|
||||||
|
StatusCodes.Status429TooManyRequests),
|
||||||
|
_ => new GlobalLeaksValidationException(
|
||||||
|
string.IsNullOrWhiteSpace(body)
|
||||||
|
? $"Login fallido (código {(int)authResponse.StatusCode})."
|
||||||
|
: $"Login fallido (código {(int)authResponse.StatusCode}): {body}",
|
||||||
|
(int)authResponse.StatusCode),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var authBody = await authResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var session = ParseAuthSession(authBody, username);
|
||||||
|
_logger.LogInformation("Login GlobalLeaks correcto para {Username}. Rol: {Role}", session.Username, session.Role ?? "(sin rol)");
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ContextDto>> GetContextsAsync(
|
||||||
|
string sessionId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using var request = CreateRequest(HttpMethod.Get, "/api/public");
|
||||||
|
using var response = await SendGlRequestAsync(request, cancellationToken);
|
||||||
|
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var contexts = ParseContexts(body);
|
||||||
|
_logger.LogInformation("GlobalLeaks /api/public devolvió {Count} contextos", contexts.Count);
|
||||||
|
return contexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ReportDto>> GetReportsAsync(
|
||||||
|
string sessionId,
|
||||||
|
string? filter,
|
||||||
|
string? dateFrom,
|
||||||
|
string? dateTo,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
filter ??= "all";
|
||||||
|
|
||||||
|
using var reportsRequest = CreateAuthenticatedRequest(HttpMethod.Get, "/api/recipient/rtips", sessionId);
|
||||||
|
using var reportsResponse = await SendGlRequestAsync(reportsRequest, cancellationToken);
|
||||||
|
var body = await reportsResponse.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var tips = ParseReports(body);
|
||||||
|
_logger.LogInformation("GlobalLeaks /api/recipient/rtips devolvió {Count} denuncias", tips.Count);
|
||||||
|
|
||||||
|
var contexts = await GetContextsAsync(sessionId, cancellationToken);
|
||||||
|
var contextNames = contexts.ToDictionary(c => c.Id, c => c.Name, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
IEnumerable<RawReport> filtered = tips;
|
||||||
|
|
||||||
|
filtered = filter switch
|
||||||
|
{
|
||||||
|
"new" => filtered.Where(t => string.IsNullOrWhiteSpace(t.AccessDate) || t.Status == "new"),
|
||||||
|
"updated" or "updated_citizen" or "updated_receiver" => filtered.Where(t => t.Updated),
|
||||||
|
_ => filtered,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(dateFrom))
|
||||||
|
{
|
||||||
|
if (!DateOnly.TryParseExact(dateFrom, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var from))
|
||||||
|
{
|
||||||
|
throw new GlobalLeaksValidationException("Formato de fecha inválido para date_from (use YYYY-MM-DD)");
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = filtered.Where(t =>
|
||||||
|
{
|
||||||
|
var date = ParseDate(t.CreationDate);
|
||||||
|
return date is not null && DateOnly.FromDateTime(date.Value.DateTime) >= from;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(dateTo))
|
||||||
|
{
|
||||||
|
if (!DateOnly.TryParseExact(dateTo, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var to))
|
||||||
|
{
|
||||||
|
throw new GlobalLeaksValidationException("Formato de fecha inválido para date_to (use YYYY-MM-DD)");
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = filtered.Where(t =>
|
||||||
|
{
|
||||||
|
var date = ParseDate(t.CreationDate);
|
||||||
|
return date is not null && DateOnly.FromDateTime(date.Value.DateTime) <= to;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
.OrderByDescending(t => t.Progressive ?? 0)
|
||||||
|
.Select(t => new ReportDto
|
||||||
|
{
|
||||||
|
Id = t.Id,
|
||||||
|
Progressive = t.Progressive,
|
||||||
|
ContextId = t.ContextId,
|
||||||
|
ContextName = t.ContextId is not null && contextNames.TryGetValue(t.ContextId, out var name)
|
||||||
|
? name
|
||||||
|
: t.ContextId,
|
||||||
|
CreationDate = t.CreationDate,
|
||||||
|
UpdateDate = t.UpdateDate,
|
||||||
|
ExpirationDate = t.ExpirationDate,
|
||||||
|
ReminderDate = t.ReminderDate,
|
||||||
|
AccessDate = t.AccessDate,
|
||||||
|
LastAccess = t.LastAccess,
|
||||||
|
Status = t.Status,
|
||||||
|
Updated = t.Updated,
|
||||||
|
Label = t.Label,
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ReportSummaryDto> GetReportSummaryAsync(
|
||||||
|
string sessionId,
|
||||||
|
string reportId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ValidateUuid(reportId);
|
||||||
|
|
||||||
|
var reports = await GetReportsAsync(sessionId, "all", null, null, cancellationToken);
|
||||||
|
var tip = reports.FirstOrDefault(report => report.Id == reportId)
|
||||||
|
?? throw new GlobalLeaksValidationException("Tip no encontrado", StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
|
var created = ParseDate(tip.CreationDate);
|
||||||
|
var updated = ParseDate(tip.UpdateDate);
|
||||||
|
var lastAccess = ParseDate(tip.LastAccess);
|
||||||
|
|
||||||
|
return new ReportSummaryDto
|
||||||
|
{
|
||||||
|
Id = tip.Id,
|
||||||
|
Progressive = tip.Progressive,
|
||||||
|
CreationDate = tip.CreationDate,
|
||||||
|
UpdateDate = tip.UpdateDate,
|
||||||
|
LastAccess = tip.LastAccess,
|
||||||
|
AccessDate = tip.AccessDate,
|
||||||
|
DaysSinceCreation = created is not null && updated is not null
|
||||||
|
? (updated.Value - created.Value).Days
|
||||||
|
: null,
|
||||||
|
DaysSinceAccess = lastAccess is not null && updated is not null
|
||||||
|
? (updated.Value - lastAccess.Value).Days
|
||||||
|
: null,
|
||||||
|
Status = tip.Status,
|
||||||
|
Label = tip.Label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FileDownloadResult> DownloadReportZipAsync(
|
||||||
|
string sessionId,
|
||||||
|
string reportId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ValidateUuid(reportId);
|
||||||
|
|
||||||
|
using var request = CreateAuthenticatedRequest(
|
||||||
|
HttpMethod.Get,
|
||||||
|
$"/api/recipient/rtips/{reportId}/export",
|
||||||
|
sessionId);
|
||||||
|
|
||||||
|
using var response = await SendGlRequestAsync(
|
||||||
|
request,
|
||||||
|
cancellationToken,
|
||||||
|
HttpCompletionOption.ResponseHeadersRead);
|
||||||
|
|
||||||
|
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||||
|
if (content.Length > _options.MaxDownloadBytes)
|
||||||
|
{
|
||||||
|
throw new GlobalLeaksValidationException("El fichero supera el límite de 500 MB.", 413);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = SanitizeFileName(
|
||||||
|
ExtractFileName(response.Content.Headers.ContentDisposition),
|
||||||
|
$"report-{reportId}.zip");
|
||||||
|
|
||||||
|
return new FileDownloadResult(content, fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FileDownloadResult> ExportReportJsonAsync(
|
||||||
|
string sessionId,
|
||||||
|
string reportId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ValidateUuid(reportId);
|
||||||
|
|
||||||
|
using var request = CreateAuthenticatedRequest(HttpMethod.Get, $"/api/recipient/rtips/{reportId}", sessionId);
|
||||||
|
using var response = await SendGlRequestAsync(request, cancellationToken);
|
||||||
|
|
||||||
|
var contentType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
|
||||||
|
var content = await response.Content.ReadAsByteArrayAsync(cancellationToken);
|
||||||
|
if (!contentType.Contains("json", StringComparison.OrdinalIgnoreCase) || content.Length == 0)
|
||||||
|
{
|
||||||
|
throw new GlobalLeaksValidationException(
|
||||||
|
"GlobalLeaks no devolvió JSON (tip sin clave de descifrado).",
|
||||||
|
422);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var document = JsonDocument.Parse(content);
|
||||||
|
var progressive = document.RootElement.TryGetProperty("progressive", out var value) && value.TryGetInt32(out var number)
|
||||||
|
? number.ToString(CultureInfo.InvariantCulture)
|
||||||
|
: reportId[..8];
|
||||||
|
|
||||||
|
return new FileDownloadResult(content, $"report-{progressive}.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HttpResponseMessage> SendGlRequestAsync(
|
||||||
|
HttpRequestMessage request,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
|
||||||
|
{
|
||||||
|
var response = await _httpClient.SendAsync(request, completionOption, cancellationToken);
|
||||||
|
if ((int)response.StatusCode == 412)
|
||||||
|
{
|
||||||
|
response.Dispose();
|
||||||
|
throw new GlobalLeaksSessionExpiredException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var message = $"Error de GlobalLeaks (código {(int)response.StatusCode}).";
|
||||||
|
response.Dispose();
|
||||||
|
throw new GlobalLeaksValidationException(message, (int)response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpRequestMessage CreateRequest(HttpMethod method, string path)
|
||||||
|
{
|
||||||
|
var request = new HttpRequestMessage(method, path)
|
||||||
|
{
|
||||||
|
Version = HttpVersion.Version11,
|
||||||
|
VersionPolicy = HttpVersionPolicy.RequestVersionOrLower,
|
||||||
|
};
|
||||||
|
|
||||||
|
request.Headers.TryAddWithoutValidation("Accept", "*/*");
|
||||||
|
request.Headers.TryAddWithoutValidation("User-Agent", "python-requests/2.32.5");
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpRequestMessage CreateAuthenticatedRequest(HttpMethod method, string path, string sessionId)
|
||||||
|
{
|
||||||
|
var request = CreateRequest(method, path);
|
||||||
|
request.Headers.Add("X-Session", sessionId);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ByteArrayContent CreateJsonContent<T>(T payload)
|
||||||
|
{
|
||||||
|
var bytes = JsonSerializer.SerializeToUtf8Bytes(payload, JsonOptions);
|
||||||
|
var content = new ByteArrayContent(bytes);
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SolveProofOfWork(string tokenId, string tokenSalt)
|
||||||
|
{
|
||||||
|
var idBytes = Encoding.UTF8.GetBytes(tokenId);
|
||||||
|
var saltBytes = Convert.FromBase64String(tokenSalt).Take(16).ToArray();
|
||||||
|
var n = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var input = idBytes.Concat(Encoding.UTF8.GetBytes(n.ToString(CultureInfo.InvariantCulture))).ToArray();
|
||||||
|
var hash = ComputeArgon2(input, saltBytes, iterations: 1, memoryKb: 1024);
|
||||||
|
if (hash[^1] == 0)
|
||||||
|
{
|
||||||
|
return $"{tokenId}:{n}";
|
||||||
|
}
|
||||||
|
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DerivePassword(string password, string salt)
|
||||||
|
{
|
||||||
|
var saltBytes = Convert.FromBase64String(salt).Take(16).ToArray();
|
||||||
|
var hash = ComputeArgon2(Encoding.UTF8.GetBytes(password), saltBytes, iterations: 16, memoryKb: 131072);
|
||||||
|
return Convert.ToBase64String(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] ComputeArgon2(byte[] input, byte[] salt, int iterations, int memoryKb)
|
||||||
|
{
|
||||||
|
var argon2 = new Argon2id(input)
|
||||||
|
{
|
||||||
|
Salt = salt,
|
||||||
|
DegreeOfParallelism = 1,
|
||||||
|
Iterations = iterations,
|
||||||
|
MemorySize = memoryKb,
|
||||||
|
};
|
||||||
|
|
||||||
|
return argon2.GetBytes(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateUuid(string value)
|
||||||
|
{
|
||||||
|
if (!Guid.TryParse(value, out _))
|
||||||
|
{
|
||||||
|
throw new GlobalLeaksValidationException("ID de denuncia inválido");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset? ParseDate(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTimeOffset.TryParse(value.Replace("Z", "+00:00", StringComparison.Ordinal), out var parsed)
|
||||||
|
? parsed
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractName(JsonElement? name, string fallback)
|
||||||
|
{
|
||||||
|
if (name is null)
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.Value.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return name.Value.GetString() ?? fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.Value.ValueKind == JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
if (name.Value.TryGetProperty("es", out var es) && es.GetString() is { Length: > 0 } esName)
|
||||||
|
{
|
||||||
|
return esName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.Value.TryGetProperty("en", out var en) && en.GetString() is { Length: > 0 } enName)
|
||||||
|
{
|
||||||
|
return enName;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var property in name.Value.EnumerateObject())
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(property.Value.GetString()))
|
||||||
|
{
|
||||||
|
return property.Value.GetString()!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ContextDto> ParseContexts(string body)
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(body);
|
||||||
|
var contextsElement = document.RootElement;
|
||||||
|
|
||||||
|
if (contextsElement.ValueKind == JsonValueKind.Object &&
|
||||||
|
contextsElement.TryGetProperty("contexts", out var contextsProperty))
|
||||||
|
{
|
||||||
|
contextsElement = contextsProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contextsElement.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var contexts = new List<ContextDto>();
|
||||||
|
foreach (var item in contextsElement.EnumerateArray())
|
||||||
|
{
|
||||||
|
var id = GetString(item, "id") ?? string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonElement? name = null;
|
||||||
|
if (item.TryGetProperty("name", out var nameProp))
|
||||||
|
{
|
||||||
|
name = nameProp;
|
||||||
|
}
|
||||||
|
|
||||||
|
contexts.Add(new ContextDto(id, ExtractName(name, id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return contexts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<RawReport> ParseReports(string body)
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(body);
|
||||||
|
var root = document.RootElement;
|
||||||
|
var array = root.ValueKind == JsonValueKind.Array
|
||||||
|
? root
|
||||||
|
: FindArrayProperty(root, "rtips", "tips", "items", "data", "entries", "results");
|
||||||
|
|
||||||
|
if (array is null || array.Value.ValueKind != JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var reports = new List<RawReport>();
|
||||||
|
foreach (var item in array.Value.EnumerateArray())
|
||||||
|
{
|
||||||
|
var id = GetString(item, "id") ?? string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
reports.Add(new RawReport
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Progressive = GetInt32(item, "progressive"),
|
||||||
|
ContextId = GetString(item, "context_id", "contextId"),
|
||||||
|
CreationDate = GetString(item, "creation_date", "creationDate"),
|
||||||
|
UpdateDate = GetString(item, "update_date", "updateDate"),
|
||||||
|
ExpirationDate = GetString(item, "expiration_date", "expirationDate"),
|
||||||
|
ReminderDate = GetString(item, "reminder_date", "reminderDate"),
|
||||||
|
AccessDate = GetString(item, "access_date", "accessDate"),
|
||||||
|
LastAccess = GetString(item, "last_access", "lastAccess"),
|
||||||
|
Status = GetString(item, "status"),
|
||||||
|
Updated = GetBool(item, "updated"),
|
||||||
|
Label = GetString(item, "label"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JsonElement? FindArrayProperty(JsonElement element, params string[] names)
|
||||||
|
{
|
||||||
|
if (element.ValueKind != JsonValueKind.Object)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (element.TryGetProperty(name, out var property) && property.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetString(JsonElement element, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (element.TryGetProperty(name, out var property))
|
||||||
|
{
|
||||||
|
return property.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.String => property.GetString(),
|
||||||
|
JsonValueKind.Number => property.GetRawText(),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int? GetInt32(JsonElement element, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (element.TryGetProperty(name, out var property))
|
||||||
|
{
|
||||||
|
if (property.ValueKind == JsonValueKind.Number && property.TryGetInt32(out var value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.ValueKind == JsonValueKind.String &&
|
||||||
|
int.TryParse(property.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetBool(JsonElement element, params string[] names)
|
||||||
|
{
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (element.TryGetProperty(name, out var property))
|
||||||
|
{
|
||||||
|
if (property.ValueKind is JsonValueKind.True or JsonValueKind.False)
|
||||||
|
{
|
||||||
|
return property.GetBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.ValueKind == JsonValueKind.String &&
|
||||||
|
bool.TryParse(property.GetString(), out var value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeFileName(string? name, string fallback)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sanitized = Regex.Replace(name, "[\\r\\n\\0\"\\\\]", "_").Trim();
|
||||||
|
return string.IsNullOrWhiteSpace(sanitized)
|
||||||
|
? fallback
|
||||||
|
: sanitized[..Math.Min(200, sanitized.Length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ExtractFileName(ContentDispositionHeaderValue? contentDisposition)
|
||||||
|
=> contentDisposition?.FileNameStar ?? contentDisposition?.FileName?.Trim('"');
|
||||||
|
|
||||||
|
private static GlSession ParseAuthSession(string body, string fallbackUsername)
|
||||||
|
{
|
||||||
|
using var document = JsonDocument.Parse(body);
|
||||||
|
var root = document.RootElement;
|
||||||
|
|
||||||
|
var id = GetString(root, "id")
|
||||||
|
?? throw new GlobalLeaksValidationException("GlobalLeaks no devolvió una sesión válida.", 502);
|
||||||
|
var username = GetString(root, "username", "name");
|
||||||
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
|
{
|
||||||
|
username = fallbackUsername.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var role = GetString(root, "role", "user_role", "userRole");
|
||||||
|
|
||||||
|
return new GlSession(id, username, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task EnsureSuccessOrThrowAsync(
|
||||||
|
HttpResponseMessage response,
|
||||||
|
string endpoint,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = await ReadBodySafeAsync(response, cancellationToken);
|
||||||
|
throw new GlobalLeaksValidationException(
|
||||||
|
string.IsNullOrWhiteSpace(body)
|
||||||
|
? $"Error en {endpoint} (código {(int)response.StatusCode})."
|
||||||
|
: $"Error en {endpoint} (código {(int)response.StatusCode}): {body}",
|
||||||
|
(int)response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadBodySafeAsync(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return (await response.Content.ReadAsStringAsync(cancellationToken)).Trim();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record TokenResponse(string Id, string Salt);
|
||||||
|
private sealed record AuthTypeResponse(string Type, string Salt);
|
||||||
|
|
||||||
|
private sealed record RawReport
|
||||||
|
{
|
||||||
|
public string Id { get; init; } = string.Empty;
|
||||||
|
public int? Progressive { get; init; }
|
||||||
|
public string? ContextId { get; init; }
|
||||||
|
public string? CreationDate { get; init; }
|
||||||
|
public string? UpdateDate { get; init; }
|
||||||
|
public string? ExpirationDate { get; init; }
|
||||||
|
public string? ReminderDate { get; init; }
|
||||||
|
public string? AccessDate { get; init; }
|
||||||
|
public string? LastAccess { get; init; }
|
||||||
|
public string? Status { get; init; }
|
||||||
|
public bool Updated { get; init; }
|
||||||
|
public string? Label { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace GestionaDenunciasAN.Services;
|
||||||
|
|
||||||
|
public sealed class GlobalLeaksValidationException(string message, int statusCode = 400) : Exception(message)
|
||||||
|
{
|
||||||
|
public int StatusCode { get; } = statusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class GlobalLeaksSessionExpiredException() : Exception("GlobaLeaks session expired.");
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using GestionaDenunciasAN.Models;
|
||||||
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Services;
|
||||||
|
|
||||||
|
public sealed class GlobalLeaksSessionStore
|
||||||
|
{
|
||||||
|
private const string RootPath = @"C:\ZipsDenuncias\.gl-auth";
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
WriteIndented = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||||
|
private readonly IDataProtector _protector;
|
||||||
|
|
||||||
|
public GlobalLeaksSessionStore(IDataProtectionProvider dataProtectionProvider)
|
||||||
|
{
|
||||||
|
_protector = dataProtectionProvider.CreateProtector("GestionaDenunciasAN.GlobalLeaksSessionStore");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<GlobalLeaksStoredSession?> GetAsync(string username, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = GetFilePath(username);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _gate.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var protectedBytes = await File.ReadAllBytesAsync(path, cancellationToken);
|
||||||
|
var protectedBase64 = Encoding.UTF8.GetString(protectedBytes);
|
||||||
|
var json = _protector.Unprotect(protectedBase64);
|
||||||
|
return JsonSerializer.Deserialize<GlobalLeaksStoredSession>(json, JsonOptions);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAsync(
|
||||||
|
string username,
|
||||||
|
string password,
|
||||||
|
string sessionId,
|
||||||
|
string? role,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var data = new GlobalLeaksStoredSession
|
||||||
|
{
|
||||||
|
Username = username,
|
||||||
|
Password = password,
|
||||||
|
SessionId = sessionId,
|
||||||
|
Role = role,
|
||||||
|
UpdatedAt = DateTimeOffset.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
await WriteAsync(data, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateSessionAsync(
|
||||||
|
string username,
|
||||||
|
string sessionId,
|
||||||
|
string? role,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var current = await GetAsync(username, cancellationToken)
|
||||||
|
?? throw new InvalidOperationException("No hay credenciales guardadas para este usuario.");
|
||||||
|
|
||||||
|
current.SessionId = sessionId;
|
||||||
|
current.Role = role;
|
||||||
|
current.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
await WriteAsync(current, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearSessionAsync(string username, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var current = await GetAsync(username, cancellationToken);
|
||||||
|
if (current is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
current.SessionId = null;
|
||||||
|
current.UpdatedAt = DateTimeOffset.UtcNow;
|
||||||
|
await WriteAsync(current, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(string username, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = GetFilePath(username);
|
||||||
|
|
||||||
|
await _gate.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WriteAsync(GlobalLeaksStoredSession data, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(RootPath);
|
||||||
|
|
||||||
|
var path = GetFilePath(data.Username);
|
||||||
|
var json = JsonSerializer.Serialize(data, JsonOptions);
|
||||||
|
var protectedValue = _protector.Protect(json);
|
||||||
|
var protectedBytes = Encoding.UTF8.GetBytes(protectedValue);
|
||||||
|
|
||||||
|
await _gate.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await File.WriteAllBytesAsync(path, protectedBytes, cancellationToken);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFilePath(string username)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(RootPath);
|
||||||
|
var normalized = username.Trim().ToLowerInvariant();
|
||||||
|
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(normalized));
|
||||||
|
return Path.Combine(RootPath, $"{Convert.ToHexString(hash).ToLowerInvariant()}.bin");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Services;
|
||||||
|
|
||||||
|
public interface IDenunciaStore
|
||||||
|
{
|
||||||
|
Task EnsureSchemaAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<List<DenunciasGestiona>> GetAllDenunciasAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<List<FicherosDenuncias>> GetAllFicherosAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<List<FicherosDenuncias>> GetFicherosByDenunciaAsync(int denunciaId, CancellationToken cancellationToken = default);
|
||||||
|
Task<DenunciasGestiona?> GetDenunciaByIdAsync(int denunciaId, CancellationToken cancellationToken = default);
|
||||||
|
Task UpsertDenunciaAsync(DenunciasGestiona denuncia, CancellationToken cancellationToken = default);
|
||||||
|
Task UpsertFicherosAsync(IEnumerable<FicherosDenuncias> ficheros, CancellationToken cancellationToken = default);
|
||||||
|
Task MarkFicherosAsUploadedAsync(
|
||||||
|
int denunciaId,
|
||||||
|
IEnumerable<string> fileNames,
|
||||||
|
DateTime uploadedAtUtc,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
131
Antifraude.Net/GestionaDenunciasAN/Services/IGestionaService.cs
Normal file
131
Antifraude.Net/GestionaDenunciasAN/Services/IGestionaService.cs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
using GestionaDenunciasAN.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Services
|
||||||
|
{
|
||||||
|
public interface IGestionaService
|
||||||
|
{
|
||||||
|
// =========================
|
||||||
|
// Expedientes (files)
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea un expediente para el procedimiento indicado y devuelve la URL del recurso 'file'.
|
||||||
|
/// </summary>
|
||||||
|
Task<string> CreateFileAsync(
|
||||||
|
Guid procedureId,
|
||||||
|
string subject,
|
||||||
|
string documentSeries,
|
||||||
|
string siaCode
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abre el expediente (lo pone en OPEN_EDITABLE), asigna título, clasificación y lo vincula al grupo indicado.
|
||||||
|
/// </summary>
|
||||||
|
Task OpenFileAsync(
|
||||||
|
string fileUrl,
|
||||||
|
Guid managementUnitGroupId,
|
||||||
|
Guid assignedGroupId,
|
||||||
|
bool confidential,
|
||||||
|
string freeTitle,
|
||||||
|
string siaCode
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Documentos
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea un recurso de upload, sube bytes y devuelve la URI de upload.
|
||||||
|
/// </summary>
|
||||||
|
Task<string> CreateUploadAsync(
|
||||||
|
byte[] contentBytes,
|
||||||
|
string fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea el documento (metadata) y sube el contenido PDF a la raíz o a una carpeta.
|
||||||
|
/// </summary>
|
||||||
|
Task UploadDocumentAsync(
|
||||||
|
string fileUrl,
|
||||||
|
byte[] contentBytes,
|
||||||
|
string fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea una carpeta de nombre 'folderName' en la raíz del expediente y devuelve su GUID.
|
||||||
|
/// </summary>
|
||||||
|
Task<Guid> CreateFolderAsync(
|
||||||
|
string fileUrl,
|
||||||
|
string folderName
|
||||||
|
);
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// Terceros
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Busca un tercero por NIF. Devuelve su ID y href si existe, null si no existe.
|
||||||
|
/// </summary>
|
||||||
|
Task<(string Id, string SelfHref)> BuscarTerceroPorNifAsync(string nif);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea un tercero en Gestiona y devuelve su ID y href.
|
||||||
|
/// </summary>
|
||||||
|
Task<(string Id, string SelfHref)> CrearTerceroAsync(ThirdPartyIdentityData thirdParty);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene la lista de terceros ya enlazados a un expediente.
|
||||||
|
/// </summary>
|
||||||
|
Task<HashSet<string>> ObtenerTercerosEnlazadosAsync(string fileUrl);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enlaza un tercero ya existente (por su href) a un expediente.
|
||||||
|
/// </summary>
|
||||||
|
Task EnlazarTerceroExistenteAsync(string fileUrl, string thirdSelfHref);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Usa el NIF tal cual viene.
|
||||||
|
/// Si es anónimo o vacío → no crea ni enlaza.
|
||||||
|
/// Si no existe, lo crea.
|
||||||
|
/// Si no está enlazado al expediente, lo enlaza.
|
||||||
|
/// </summary>
|
||||||
|
Task AsegurarTerceroYEnlazarAsync(string fileUrl, ThirdPartyIdentityData thirdParty);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// =========================
|
||||||
|
// CONSULTAS OPERATIVAS
|
||||||
|
// =========================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Devuelve el JSON crudo del listado operativo de expedientes, sin recorrer histórico paginado.
|
||||||
|
/// </summary>
|
||||||
|
Task<string> ListarExpedientesJsonAsyncBasico(int maxPages = 1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
Task<GestionaExpedienteInfo?> BuscarExpedientePorIdEnAsuntoAsync(int idDenuncia);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Obtiene los metadatos visibles de un expediente concreto.
|
||||||
|
/// </summary>
|
||||||
|
Task<GestionaExpedienteInfo?> ObtenerExpedienteAsync(string fileUrl);
|
||||||
|
|
||||||
|
Task<List<ExpedienteTerceroDto>> ObtenerExpedientesPorTerceroAsync(
|
||||||
|
string nif,
|
||||||
|
DateTimeOffset? desde = null,
|
||||||
|
DateTimeOffset? hasta = null,
|
||||||
|
int maxPages = 1,
|
||||||
|
int maxResults = 30,
|
||||||
|
int maxParallel = 6
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using GestionaDenunciasAN.Models;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Services;
|
||||||
|
|
||||||
|
public interface IInboxTrackingService
|
||||||
|
{
|
||||||
|
Task<InboxUserState> GetUserStateAsync(string username, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<ReportDto>> RegisterSnapshotAsync(
|
||||||
|
string username,
|
||||||
|
IEnumerable<ReportDto> reports,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
Task MarkReportImportedAsync(
|
||||||
|
string username,
|
||||||
|
ReportDto report,
|
||||||
|
int? complaintId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,522 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using GestionaDenunciasAN.Configuration;
|
||||||
|
using GestionaDenunciasAN.Models;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MySqlConnector;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Services;
|
||||||
|
|
||||||
|
public sealed class InboxTrackingService : IInboxTrackingService
|
||||||
|
{
|
||||||
|
private readonly ComplaintStorageOptions _options;
|
||||||
|
private readonly IDenunciaStore _denunciaStore;
|
||||||
|
|
||||||
|
public InboxTrackingService(
|
||||||
|
IOptions<ComplaintStorageOptions> options,
|
||||||
|
IDenunciaStore denunciaStore)
|
||||||
|
{
|
||||||
|
_options = options.Value;
|
||||||
|
_denunciaStore = denunciaStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<InboxUserState> GetUserStateAsync(string username, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
|
{
|
||||||
|
return new InboxUserState();
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||||
|
var userId = await EnsureUserAsync(connection, username, cancellationToken);
|
||||||
|
|
||||||
|
const string sql = """
|
||||||
|
SELECT
|
||||||
|
username,
|
||||||
|
last_successful_download_at_utc,
|
||||||
|
last_downloaded_report_moment_utc
|
||||||
|
FROM app_users
|
||||||
|
WHERE id = @userId;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var command = new MySqlCommand(sql, connection);
|
||||||
|
command.Parameters.AddWithValue("@userId", userId);
|
||||||
|
|
||||||
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||||
|
if (!await reader.ReadAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
return new InboxUserState { Username = username };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InboxUserState
|
||||||
|
{
|
||||||
|
Username = reader.GetString(reader.GetOrdinal("username")),
|
||||||
|
LastSuccessfulDownloadAtUtc = GetDateTimeOffset(reader, "last_successful_download_at_utc"),
|
||||||
|
LastDownloadedReportMomentUtc = GetDateTimeOffset(reader, "last_downloaded_report_moment_utc"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<ReportDto>> RegisterSnapshotAsync(
|
||||||
|
string username,
|
||||||
|
IEnumerable<ReportDto> reports,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||||
|
|
||||||
|
var reportList = reports.ToList();
|
||||||
|
if (string.IsNullOrWhiteSpace(username) || reportList.Count == 0)
|
||||||
|
{
|
||||||
|
return reportList;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||||
|
var userId = await EnsureUserAsync(connection, username, cancellationToken);
|
||||||
|
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var report in reportList)
|
||||||
|
{
|
||||||
|
await UpsertInboxReportAsync(connection, transaction, report, cancellationToken);
|
||||||
|
await UpsertUserSnapshotAsync(connection, transaction, userId, report.Id, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = await LoadMetadataAsync(connection, userId, reportList.Select(r => r.Id).ToList(), cancellationToken);
|
||||||
|
|
||||||
|
return reportList
|
||||||
|
.Select(report =>
|
||||||
|
{
|
||||||
|
metadata.TryGetValue(report.Id, out var meta);
|
||||||
|
return report with
|
||||||
|
{
|
||||||
|
DownloadedByCurrentUser = meta?.DownloadedByCurrentUser ?? false,
|
||||||
|
DownloadedByAnotherUser = meta?.DownloadedByAnotherUser ?? false,
|
||||||
|
LastDownloadedByUsername = meta?.LastDownloadedByUsername,
|
||||||
|
LastDownloadedAt = meta?.LastDownloadedAtUtc?.ToString("O", CultureInfo.InvariantCulture),
|
||||||
|
AlreadyImported = meta?.AlreadyImported ?? false,
|
||||||
|
AlreadyInGestiona = meta?.AlreadyInGestiona ?? false,
|
||||||
|
TrackingNote = BuildTrackingNote(meta)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task MarkReportImportedAsync(
|
||||||
|
string username,
|
||||||
|
ReportDto report,
|
||||||
|
int? complaintId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(report.Id))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reportMoment = ResolveReportMoment(report);
|
||||||
|
var nowUtc = DateTime.UtcNow;
|
||||||
|
|
||||||
|
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, transaction, report, cancellationToken);
|
||||||
|
|
||||||
|
const string updateInboxSql = """
|
||||||
|
UPDATE inbox_reports
|
||||||
|
SET
|
||||||
|
last_downloaded_at_utc = @nowUtc,
|
||||||
|
last_downloaded_by_user_id = @userId,
|
||||||
|
imported_complaint_report_id = COALESCE(@complaintId, imported_complaint_report_id),
|
||||||
|
imported_to_store_at_utc = COALESCE(imported_to_store_at_utc, @nowUtc),
|
||||||
|
updated_at_utc = CURRENT_TIMESTAMP(6)
|
||||||
|
WHERE global_report_uuid = @reportId;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using (var updateInbox = new MySqlCommand(updateInboxSql, connection, (MySqlTransaction)transaction))
|
||||||
|
{
|
||||||
|
updateInbox.Parameters.AddWithValue("@nowUtc", nowUtc);
|
||||||
|
updateInbox.Parameters.AddWithValue("@userId", userId);
|
||||||
|
updateInbox.Parameters.AddWithValue("@complaintId", complaintId.HasValue ? complaintId.Value : DBNull.Value);
|
||||||
|
updateInbox.Parameters.AddWithValue("@reportId", report.Id);
|
||||||
|
await updateInbox.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
const string updateUserReportSql = """
|
||||||
|
INSERT INTO user_inbox_reports (
|
||||||
|
app_user_id,
|
||||||
|
inbox_report_id,
|
||||||
|
first_seen_at_utc,
|
||||||
|
last_seen_at_utc,
|
||||||
|
first_downloaded_at_utc,
|
||||||
|
last_downloaded_at_utc,
|
||||||
|
download_count
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
@userId,
|
||||||
|
ir.id,
|
||||||
|
CURRENT_TIMESTAMP(6),
|
||||||
|
CURRENT_TIMESTAMP(6),
|
||||||
|
@nowUtc,
|
||||||
|
@nowUtc,
|
||||||
|
1
|
||||||
|
FROM inbox_reports ir
|
||||||
|
WHERE ir.global_report_uuid = @reportId
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
last_seen_at_utc = CURRENT_TIMESTAMP(6),
|
||||||
|
first_downloaded_at_utc = COALESCE(first_downloaded_at_utc, VALUES(first_downloaded_at_utc)),
|
||||||
|
last_downloaded_at_utc = VALUES(last_downloaded_at_utc),
|
||||||
|
download_count = download_count + 1;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using (var updateUserReport = new MySqlCommand(updateUserReportSql, connection, (MySqlTransaction)transaction))
|
||||||
|
{
|
||||||
|
updateUserReport.Parameters.AddWithValue("@userId", userId);
|
||||||
|
updateUserReport.Parameters.AddWithValue("@nowUtc", nowUtc);
|
||||||
|
updateUserReport.Parameters.AddWithValue("@reportId", report.Id);
|
||||||
|
await updateUserReport.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
const string updateUserSql = """
|
||||||
|
UPDATE app_users
|
||||||
|
SET
|
||||||
|
last_successful_download_at_utc = @nowUtc,
|
||||||
|
last_downloaded_report_moment_utc =
|
||||||
|
CASE
|
||||||
|
WHEN @reportMoment IS NULL THEN last_downloaded_report_moment_utc
|
||||||
|
WHEN last_downloaded_report_moment_utc IS NULL THEN @reportMoment
|
||||||
|
WHEN @reportMoment > last_downloaded_report_moment_utc THEN @reportMoment
|
||||||
|
ELSE last_downloaded_report_moment_utc
|
||||||
|
END,
|
||||||
|
updated_at_utc = CURRENT_TIMESTAMP(6)
|
||||||
|
WHERE id = @userId;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using (var updateUser = new MySqlCommand(updateUserSql, connection, (MySqlTransaction)transaction))
|
||||||
|
{
|
||||||
|
updateUser.Parameters.AddWithValue("@nowUtc", nowUtc);
|
||||||
|
updateUser.Parameters.AddWithValue("@reportMoment", ToDbDate(reportMoment));
|
||||||
|
updateUser.Parameters.AddWithValue("@userId", userId);
|
||||||
|
await updateUser.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<long> EnsureUserAsync(MySqlConnection connection, string username, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string insertSql = """
|
||||||
|
INSERT INTO app_users (username)
|
||||||
|
VALUES (@username)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
updated_at_utc = CURRENT_TIMESTAMP(6);
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using (var insert = new MySqlCommand(insertSql, connection))
|
||||||
|
{
|
||||||
|
insert.Parameters.AddWithValue("@username", username.Trim());
|
||||||
|
await insert.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
const string selectSql = """
|
||||||
|
SELECT id
|
||||||
|
FROM app_users
|
||||||
|
WHERE username = @username
|
||||||
|
LIMIT 1;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var select = new MySqlCommand(selectSql, connection);
|
||||||
|
select.Parameters.AddWithValue("@username", username.Trim());
|
||||||
|
var result = await select.ExecuteScalarAsync(cancellationToken);
|
||||||
|
return Convert.ToInt64(result, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task UpsertInboxReportAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
MySqlTransaction transaction,
|
||||||
|
ReportDto report,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
INSERT INTO inbox_reports (
|
||||||
|
global_report_uuid,
|
||||||
|
progressive_id,
|
||||||
|
context_id,
|
||||||
|
context_name,
|
||||||
|
creation_date_utc,
|
||||||
|
update_date_utc,
|
||||||
|
access_date_utc,
|
||||||
|
last_access_utc,
|
||||||
|
gl_status,
|
||||||
|
gl_label,
|
||||||
|
is_updated
|
||||||
|
) VALUES (
|
||||||
|
@reportId,
|
||||||
|
@progressiveId,
|
||||||
|
@contextId,
|
||||||
|
@contextName,
|
||||||
|
@creationDateUtc,
|
||||||
|
@updateDateUtc,
|
||||||
|
@accessDateUtc,
|
||||||
|
@lastAccessUtc,
|
||||||
|
@glStatus,
|
||||||
|
@glLabel,
|
||||||
|
@isUpdated
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
progressive_id = VALUES(progressive_id),
|
||||||
|
context_id = VALUES(context_id),
|
||||||
|
context_name = VALUES(context_name),
|
||||||
|
creation_date_utc = VALUES(creation_date_utc),
|
||||||
|
update_date_utc = VALUES(update_date_utc),
|
||||||
|
access_date_utc = VALUES(access_date_utc),
|
||||||
|
last_access_utc = VALUES(last_access_utc),
|
||||||
|
gl_status = VALUES(gl_status),
|
||||||
|
gl_label = VALUES(gl_label),
|
||||||
|
is_updated = VALUES(is_updated),
|
||||||
|
last_seen_at_utc = CURRENT_TIMESTAMP(6),
|
||||||
|
updated_at_utc = CURRENT_TIMESTAMP(6);
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var command = new MySqlCommand(sql, connection, transaction);
|
||||||
|
command.Parameters.AddWithValue("@reportId", report.Id);
|
||||||
|
command.Parameters.AddWithValue("@progressiveId", report.Progressive.HasValue ? report.Progressive.Value : DBNull.Value);
|
||||||
|
command.Parameters.AddWithValue("@contextId", ToDbString(report.ContextId));
|
||||||
|
command.Parameters.AddWithValue("@contextName", ToDbString(report.ContextName));
|
||||||
|
command.Parameters.AddWithValue("@creationDateUtc", ToDbDate(ParseDate(report.CreationDate)));
|
||||||
|
command.Parameters.AddWithValue("@updateDateUtc", ToDbDate(ParseDate(report.UpdateDate)));
|
||||||
|
command.Parameters.AddWithValue("@accessDateUtc", ToDbDate(ParseDate(report.AccessDate)));
|
||||||
|
command.Parameters.AddWithValue("@lastAccessUtc", ToDbDate(ParseDate(report.LastAccess)));
|
||||||
|
command.Parameters.AddWithValue("@glStatus", ToDbString(report.Status));
|
||||||
|
command.Parameters.AddWithValue("@glLabel", ToDbString(report.Label));
|
||||||
|
command.Parameters.AddWithValue("@isUpdated", report.Updated);
|
||||||
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task UpsertUserSnapshotAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
MySqlTransaction transaction,
|
||||||
|
long userId,
|
||||||
|
string reportId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
INSERT INTO user_inbox_reports (
|
||||||
|
app_user_id,
|
||||||
|
inbox_report_id,
|
||||||
|
first_seen_at_utc,
|
||||||
|
last_seen_at_utc
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
@userId,
|
||||||
|
ir.id,
|
||||||
|
CURRENT_TIMESTAMP(6),
|
||||||
|
CURRENT_TIMESTAMP(6)
|
||||||
|
FROM inbox_reports ir
|
||||||
|
WHERE ir.global_report_uuid = @reportId
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
last_seen_at_utc = CURRENT_TIMESTAMP(6);
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var command = new MySqlCommand(sql, connection, transaction);
|
||||||
|
command.Parameters.AddWithValue("@userId", userId);
|
||||||
|
command.Parameters.AddWithValue("@reportId", reportId);
|
||||||
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<string, ReportMetadata>> LoadMetadataAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
long userId,
|
||||||
|
List<string> reportIds,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var metadata = new Dictionary<string, ReportMetadata>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (reportIds.Count == 0)
|
||||||
|
{
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var command = connection.CreateCommand();
|
||||||
|
var parameterNames = new List<string>(reportIds.Count);
|
||||||
|
for (var i = 0; i < reportIds.Count; i++)
|
||||||
|
{
|
||||||
|
var parameterName = $"@reportId{i}";
|
||||||
|
parameterNames.Add(parameterName);
|
||||||
|
command.Parameters.AddWithValue(parameterName, reportIds[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
command.Parameters.AddWithValue("@userId", userId);
|
||||||
|
command.CommandText = $"""
|
||||||
|
SELECT
|
||||||
|
ir.global_report_uuid,
|
||||||
|
ir.last_downloaded_at_utc,
|
||||||
|
downloader.username AS last_downloaded_by_username,
|
||||||
|
ir.imported_to_store_at_utc,
|
||||||
|
COALESCE(c.is_in_gestiona, 0) AS already_in_gestiona,
|
||||||
|
CASE WHEN uir.last_downloaded_at_utc IS NULL THEN 0 ELSE 1 END AS downloaded_by_current_user
|
||||||
|
FROM inbox_reports ir
|
||||||
|
LEFT JOIN app_users downloader ON downloader.id = ir.last_downloaded_by_user_id
|
||||||
|
LEFT JOIN user_inbox_reports uir
|
||||||
|
ON uir.inbox_report_id = ir.id
|
||||||
|
AND uir.app_user_id = @userId
|
||||||
|
LEFT JOIN complaints c
|
||||||
|
ON c.external_report_id = COALESCE(ir.imported_complaint_report_id, ir.progressive_id)
|
||||||
|
WHERE ir.global_report_uuid IN ({string.Join(", ", parameterNames)});
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||||
|
while (await reader.ReadAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
var reportId = GetStringValue(reader, "global_report_uuid");
|
||||||
|
var lastDownloadedByUsername = reader.IsDBNull(reader.GetOrdinal("last_downloaded_by_username"))
|
||||||
|
? 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);
|
||||||
|
|
||||||
|
metadata[reportId] = new ReportMetadata
|
||||||
|
{
|
||||||
|
DownloadedByCurrentUser = downloadedByCurrentUser,
|
||||||
|
DownloadedByAnotherUser = downloadedByAnotherUser,
|
||||||
|
LastDownloadedByUsername = lastDownloadedByUsername,
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_options.ConnectionString))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Falta configurar ComplaintStorage:ConnectionString en appsettings.json.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var connection = new MySqlConnection(_options.ConnectionString);
|
||||||
|
await connection.OpenAsync(cancellationToken);
|
||||||
|
await using var timeZoneCommand = new MySqlCommand("SET time_zone = '+00:00';", connection);
|
||||||
|
await timeZoneCommand.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ToDbString(string? value)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? DBNull.Value : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object ToDbDate(DateTimeOffset? value)
|
||||||
|
{
|
||||||
|
return value is null ? DBNull.Value : value.Value.UtcDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset? ParseDate(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTimeOffset.TryParse(value.Replace("Z", "+00:00", StringComparison.Ordinal), out var parsed)
|
||||||
|
? parsed
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset? ResolveReportMoment(ReportDto report)
|
||||||
|
{
|
||||||
|
return ParseDate(report.UpdateDate) ?? ParseDate(report.CreationDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTimeOffset? GetDateTimeOffset(MySqlDataReader reader, string columnName)
|
||||||
|
{
|
||||||
|
var ordinal = reader.GetOrdinal(columnName);
|
||||||
|
return reader.IsDBNull(ordinal)
|
||||||
|
? null
|
||||||
|
: new DateTimeOffset(DateTime.SpecifyKind(reader.GetDateTime(ordinal), DateTimeKind.Utc));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetStringValue(MySqlDataReader reader, string columnName)
|
||||||
|
{
|
||||||
|
var ordinal = reader.GetOrdinal(columnName);
|
||||||
|
if (reader.IsDBNull(ordinal))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = reader.GetValue(ordinal);
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
Guid guid => guid.ToString("D"),
|
||||||
|
string text => text,
|
||||||
|
_ => Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? BuildTrackingNote(ReportMetadata? metadata)
|
||||||
|
{
|
||||||
|
if (metadata is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.AlreadyInGestiona)
|
||||||
|
{
|
||||||
|
return "Ya existe expediente en Gestiona";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.DownloadedByAnotherUser)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(metadata.LastDownloadedByUsername) && metadata.LastDownloadedAtUtc is not null)
|
||||||
|
{
|
||||||
|
return $"La descargó {metadata.LastDownloadedByUsername} el {metadata.LastDownloadedAtUtc.Value.ToLocalTime():dd/MM/yyyy HH:mm}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Ya la descargó otro usuario";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.DownloadedByCurrentUser && metadata.LastDownloadedAtUtc is not null)
|
||||||
|
{
|
||||||
|
return $"Ya la descargaste el {metadata.LastDownloadedAtUtc.Value.ToLocalTime():dd/MM/yyyy HH:mm}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.AlreadyImported)
|
||||||
|
{
|
||||||
|
return "Ya está incorporada a la base de trabajo";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record ReportMetadata
|
||||||
|
{
|
||||||
|
public bool DownloadedByCurrentUser { get; init; }
|
||||||
|
public bool DownloadedByAnotherUser { get; init; }
|
||||||
|
public string? LastDownloadedByUsername { get; init; }
|
||||||
|
public DateTimeOffset? LastDownloadedAtUtc { get; init; }
|
||||||
|
public bool AlreadyImported { get; init; }
|
||||||
|
public bool AlreadyInGestiona { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace GestionaDenunciasAN.Services;
|
||||||
|
|
||||||
|
public sealed class LoginRateLimiter
|
||||||
|
{
|
||||||
|
private const int MaxAttempts = 5;
|
||||||
|
private static readonly TimeSpan Window = TimeSpan.FromMinutes(1);
|
||||||
|
private readonly ConcurrentDictionary<string, List<DateTimeOffset>> _attempts = new();
|
||||||
|
private readonly object _gate = new();
|
||||||
|
|
||||||
|
public bool AllowAttempt(string key)
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
var entries = _attempts.GetOrAdd(key, _ => []);
|
||||||
|
entries.RemoveAll(t => now - t > Window);
|
||||||
|
|
||||||
|
if (entries.Count >= MaxAttempts)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Add(now);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1109
Antifraude.Net/GestionaDenunciasAN/Services/MySqlDenunciaStore.cs
Normal file
1109
Antifraude.Net/GestionaDenunciasAN/Services/MySqlDenunciaStore.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,52 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
|
"SwaggerCC": "https://sw-antifraude.tecnosis.net/api/",
|
||||||
//SWAGGER DESARROLLO
|
"Gestiona": {
|
||||||
//"SwaggerCC": "https://localhost:7135/api/",
|
"ApiBase": "https://02.g3stiona.com",
|
||||||
//SWAGGER PUBLICADO
|
//"AccessToken": "_mZcOqbvtCo2a8zIgjZZA9g__2",
|
||||||
"SwaggerCC": "https://sw-antifraude.tecnosis.net/api/"
|
//"AccessToken": "_PuH9tvEZqkhBplTMZkQmAw__1",
|
||||||
|
"AccessToken": "_yr.xVvPOllsyd1TYZRxUxg__c",
|
||||||
|
"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",
|
||||||
|
"Location": "2.02.01"
|
||||||
|
},
|
||||||
|
"GlobalLeaks": {
|
||||||
|
"BaseUrl": "https://prebuzon.antifraudeandalucia.es",
|
||||||
|
"TimeoutSeconds": 120,
|
||||||
|
"MaxDownloadBytes": 524288000
|
||||||
|
},
|
||||||
|
"ComplaintStorage": {
|
||||||
|
"ConnectionString": "Server=192.168.41.25;Port=13306;Database=gestiondenuncias;Uid=tecnosis;Pwd=tsl4net.Ts87;",
|
||||||
|
"AutoCreateSchema": true
|
||||||
|
},
|
||||||
|
"ReverseProxy": {
|
||||||
|
"Routes": {
|
||||||
|
"PortalRoute": {
|
||||||
|
"ClusterId": "PortalCluster",
|
||||||
|
"Match": { "Path": "/portal/{**catchall}" },
|
||||||
|
"Transforms": [
|
||||||
|
{ "PathRemovePrefix": "/portal" },
|
||||||
|
{ "ResponseHeaderRemove": "X-Frame-Options" },
|
||||||
|
{ "ResponseHeaderRemove": "Content-Security-Policy" },
|
||||||
|
{
|
||||||
|
"ResponseHeader": "Content-Security-Policy",
|
||||||
|
"Set": "frame-ancestors 'self'"
|
||||||
|
},
|
||||||
|
{ "RequestHeaderOriginalHost": "true" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Clusters": {
|
||||||
|
"PortalCluster": {
|
||||||
|
"Destinations": {
|
||||||
|
"d1": { "Address": "https://prebuzon.antifraudeandalucia.es/" }
|
||||||
|
},
|
||||||
|
"HttpRequest": {
|
||||||
|
"Version": "2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,475 @@
|
|||||||
html, body {
|
:root {
|
||||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
--app-ink: #12395f;
|
||||||
|
--app-ink-soft: #3a5e83;
|
||||||
|
--app-accent: #2a5298;
|
||||||
|
--app-accent-strong: #173b6d;
|
||||||
|
--app-accent-soft: #e9f1fb;
|
||||||
|
--app-sky: #5a9bd5;
|
||||||
|
--app-border: #d7e4f1;
|
||||||
|
--app-panel: rgba(255, 255, 255, 0.88);
|
||||||
|
--app-panel-strong: #ffffff;
|
||||||
|
--app-shadow: 0 24px 60px rgba(18, 57, 95, 0.12);
|
||||||
|
--app-shadow-soft: 0 12px 30px rgba(18, 57, 95, 0.08);
|
||||||
|
--app-success: #1f7a55;
|
||||||
|
--app-warning: #b7791f;
|
||||||
|
--app-danger: #b33a3a;
|
||||||
|
--app-radius-xl: 28px;
|
||||||
|
--app-radius-lg: 22px;
|
||||||
|
--app-radius-md: 16px;
|
||||||
|
--app-radius-sm: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a, .btn-link {
|
html,
|
||||||
color: #006bb7;
|
body {
|
||||||
|
font-family: "Aptos", "Segoe UI", Tahoma, sans-serif;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(90, 155, 213, 0.24), transparent 30%),
|
||||||
|
radial-gradient(circle at bottom right, rgba(42, 82, 152, 0.16), transparent 28%),
|
||||||
|
linear-gradient(135deg, #f4f8fc 0%, #e8eff7 48%, #f7fbff 100%);
|
||||||
|
color: var(--app-ink);
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
.btn-link {
|
||||||
|
color: var(--app-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
.btn-link:hover {
|
||||||
|
color: var(--app-accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
color: var(--app-ink);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: var(--app-accent-strong);
|
||||||
|
background: rgba(42, 82, 152, 0.08);
|
||||||
|
padding: 0.15rem 0.4rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background: #f7fbff;
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
border-radius: var(--app-radius-sm);
|
||||||
|
padding: 1rem;
|
||||||
|
color: var(--app-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 320px minmax(0, 1fr);
|
||||||
|
min-height: 100vh;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-main {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
padding: 1.5rem 1.75rem;
|
||||||
|
border-radius: var(--app-radius-xl);
|
||||||
|
background: linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(243, 248, 255, 0.96));
|
||||||
|
border: 1px solid rgba(90, 155, 213, 0.14);
|
||||||
|
box-shadow: var(--app-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__intro {
|
||||||
|
max-width: 58rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__eyebrow {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(42, 82, 152, 0.1);
|
||||||
|
color: var(--app-accent-strong);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__title {
|
||||||
|
margin: 0.8rem 0 0.35rem;
|
||||||
|
font-size: clamp(1.8rem, 2vw, 2.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__copy {
|
||||||
|
color: var(--app-ink-soft);
|
||||||
|
max-width: 48rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-session-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.55rem;
|
||||||
|
padding: 0.55rem 0.9rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(31, 122, 85, 0.12);
|
||||||
|
color: var(--app-success);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-session-pill__dot {
|
||||||
|
width: 0.65rem;
|
||||||
|
height: 0.65rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: currentColor;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(31, 122, 85, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-user-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.85rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
border-radius: 18px;
|
||||||
|
background: var(--app-panel-strong);
|
||||||
|
box-shadow: var(--app-shadow-soft);
|
||||||
|
color: var(--app-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-user-chip:hover,
|
||||||
|
.app-user-chip:focus-visible {
|
||||||
|
border-color: rgba(42, 82, 152, 0.35);
|
||||||
|
background: #f7fbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-user-chip__icon {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: var(--app-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-user-chip__text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-user-chip__text small {
|
||||||
|
color: var(--app-ink-soft);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content {
|
||||||
|
flex: 1;
|
||||||
|
border-radius: var(--app-radius-xl);
|
||||||
|
background: rgba(255, 255, 255, 0.58);
|
||||||
|
border: 1px solid rgba(90, 155, 213, 0.14);
|
||||||
|
box-shadow: var(--app-shadow-soft);
|
||||||
|
padding: 1.5rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content > .container,
|
||||||
|
.app-content > .container-fluid {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-shell {
|
||||||
|
position: sticky;
|
||||||
|
top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.2rem;
|
||||||
|
min-height: calc(100vh - 2rem);
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-radius: var(--app-radius-xl);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(18, 57, 95, 0.98) 0%, rgba(23, 59, 109, 0.98) 55%, rgba(16, 44, 81, 0.98) 100%);
|
||||||
|
box-shadow: var(--app-shadow);
|
||||||
|
color: #f5fbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.9rem;
|
||||||
|
border-radius: var(--app-radius-lg);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand__logo {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand__copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand__eyebrow {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(230, 244, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand__title {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand__subtitle {
|
||||||
|
font-size: 0.88rem;
|
||||||
|
color: rgba(230, 244, 255, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sections {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section--footer {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-section__label {
|
||||||
|
padding: 0 0.35rem;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: rgba(224, 239, 255, 0.58);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.85rem;
|
||||||
|
padding: 0.9rem 0.95rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
color: rgba(244, 250, 255, 0.88);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link:hover,
|
||||||
|
.menu-link:focus-visible {
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: rgba(255, 255, 255, 0.08);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link.active {
|
||||||
|
color: #0f325d;
|
||||||
|
background: linear-gradient(135deg, #ffffff 0%, #edf5ff 100%);
|
||||||
|
border-color: rgba(90, 155, 213, 0.28);
|
||||||
|
box-shadow: 0 16px 28px rgba(7, 25, 45, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link.active .menu-link__meta {
|
||||||
|
color: #53708e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link__icon {
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link.active .menu-link__icon {
|
||||||
|
background: rgba(42, 82, 152, 0.1);
|
||||||
|
color: var(--app-accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link__content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.2rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link__title {
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link__meta {
|
||||||
|
font-size: 0.83rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: rgba(226, 240, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card,
|
||||||
|
.modal-content {
|
||||||
|
border: 1px solid var(--app-border);
|
||||||
|
border-radius: var(--app-radius-lg);
|
||||||
|
background: var(--app-panel);
|
||||||
|
box-shadow: var(--app-shadow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
color: var(--app-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
border: none;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: var(--app-shadow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
--bs-table-bg: transparent;
|
||||||
|
--bs-table-striped-bg: rgba(42, 82, 152, 0.04);
|
||||||
|
--bs-table-hover-bg: rgba(42, 82, 152, 0.06);
|
||||||
|
color: var(--app-ink);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table > :not(caption) > * > * {
|
||||||
|
border-bottom-color: rgba(18, 57, 95, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--app-ink-soft);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-responsive {
|
||||||
|
border-radius: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control,
|
||||||
|
.form-select {
|
||||||
|
min-height: 2.95rem;
|
||||||
|
border-radius: 14px;
|
||||||
|
border-color: #cbd9e7;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: var(--app-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.form-control {
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus,
|
||||||
|
.form-select:focus,
|
||||||
|
.form-check-input:focus,
|
||||||
|
.btn:focus,
|
||||||
|
.btn:active:focus,
|
||||||
|
.btn-link.nav-link:focus {
|
||||||
|
border-color: rgba(42, 82, 152, 0.45);
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(42, 82, 152, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 14px;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #1b6ec2;
|
background: linear-gradient(135deg, var(--app-accent), var(--app-accent-strong));
|
||||||
border-color: #1861ac;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
.btn-primary:hover,
|
||||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
.btn-primary:focus-visible {
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #315ea9, #1a4278);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary {
|
||||||
|
color: var(--app-accent);
|
||||||
|
border-color: rgba(42, 82, 152, 0.28);
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-primary:hover,
|
||||||
|
.btn-outline-primary:focus-visible {
|
||||||
|
background: var(--app-accent-soft);
|
||||||
|
color: var(--app-accent-strong);
|
||||||
|
border-color: rgba(42, 82, 152, 0.36);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-secondary {
|
||||||
|
color: var(--app-ink);
|
||||||
|
border-color: rgba(18, 57, 95, 0.14);
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-secondary:hover,
|
||||||
|
.btn-outline-secondary:focus-visible {
|
||||||
|
color: var(--app-ink);
|
||||||
|
background: rgba(18, 57, 95, 0.06);
|
||||||
|
border-color: rgba(18, 57, 95, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 0.5rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
padding-top: 1.1rem;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1:focus {
|
h1:focus {
|
||||||
@@ -37,15 +489,89 @@ h1:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.blazor-error-boundary {
|
.blazor-error-boundary {
|
||||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
background: #b32121;
|
||||||
padding: 1rem 1rem 1rem 3.7rem;
|
padding: 1rem 1rem 1rem 3.7rem;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blazor-error-boundary::after {
|
.blazor-error-boundary::after {
|
||||||
content: "An error has occurred."
|
content: "An error has occurred.";
|
||||||
}
|
}
|
||||||
|
|
||||||
.darker-border-checkbox.form-check-input {
|
.darker-border-checkbox.form-check-input {
|
||||||
border-color: #929292;
|
border-color: #929292;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui {
|
||||||
|
background: #fff6d7;
|
||||||
|
color: #6c5400;
|
||||||
|
bottom: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
box-shadow: var(--app-shadow-soft);
|
||||||
|
display: none;
|
||||||
|
padding: 0.85rem 1.2rem;
|
||||||
|
position: fixed;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(183, 121, 31, 0.18);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.9rem;
|
||||||
|
top: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.app-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-shell {
|
||||||
|
position: static;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header__actions {
|
||||||
|
width: 100%;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-user-chip,
|
||||||
|
.app-session-pill {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-shell {
|
||||||
|
padding: 0.75rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content,
|
||||||
|
.app-header,
|
||||||
|
.nav-shell {
|
||||||
|
border-radius: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand__logo {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-link {
|
||||||
|
padding: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
43
Antifraude.Net/GestionaDenunciasAN/wwwroot/js/appAuth.js
Normal file
43
Antifraude.Net/GestionaDenunciasAN/wwwroot/js/appAuth.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
window.appAuthPostJson = async function (url, body) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = null;
|
||||||
|
try {
|
||||||
|
data = await response.json();
|
||||||
|
} catch {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
window.appAuthPost = async function (url) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin"
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = null;
|
||||||
|
try {
|
||||||
|
data = await response.json();
|
||||||
|
} catch {
|
||||||
|
data = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user