arreglo certificados de registro
This commit is contained in:
@@ -12,10 +12,14 @@ namespace ApiDenuncias.Controllers;
|
|||||||
public sealed class ConfigurationController : ControllerBase
|
public sealed class ConfigurationController : ControllerBase
|
||||||
{
|
{
|
||||||
private readonly AppConfigurationService _configurationService;
|
private readonly AppConfigurationService _configurationService;
|
||||||
|
private readonly WorkGroupAdministrationService _workGroupService;
|
||||||
|
|
||||||
public ConfigurationController(AppConfigurationService configurationService)
|
public ConfigurationController(
|
||||||
|
AppConfigurationService configurationService,
|
||||||
|
WorkGroupAdministrationService workGroupService)
|
||||||
{
|
{
|
||||||
_configurationService = configurationService;
|
_configurationService = configurationService;
|
||||||
|
_workGroupService = workGroupService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -48,4 +52,36 @@ public sealed class ConfigurationController : ControllerBase
|
|||||||
|
|
||||||
return Ok(await _configurationService.SetExternalUpdateCutoffDateAsync(date, cancellationToken));
|
return Ok(await _configurationService.SetExternalUpdateCutoffDateAsync(date, cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("work-groups")]
|
||||||
|
[Authorize(Policy = "ConfigurationAdministrators")]
|
||||||
|
public async Task<ActionResult<WorkGroupAdministrationDto>> GetWorkGroups(
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Ok(await _workGroupService.GetAsync(cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("work-groups/users/{username}")]
|
||||||
|
[Authorize(Policy = "ConfigurationAdministrators")]
|
||||||
|
public async Task<ActionResult<WorkGroupAdministrationDto>> SetUserWorkGroups(
|
||||||
|
string username,
|
||||||
|
UpdateUserWorkGroupsRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var changedBy = User.Identity?.Name ??
|
||||||
|
throw new InvalidOperationException("No hay usuario autenticado.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Ok(await _workGroupService.UpdateUserGroupsAsync(
|
||||||
|
username,
|
||||||
|
request.GroupCodes ?? [],
|
||||||
|
changedBy,
|
||||||
|
cancellationToken));
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new ApiError(ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,13 +45,22 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
[FromQuery] DenunciaListScope scope,
|
[FromQuery] DenunciaListScope scope,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var allowedIds = await GetAllowedIdsAsync(cancellationToken);
|
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||||
if (allowedIds.Count == 0)
|
var access = await _accessService.GetComplaintAccessAsync(
|
||||||
|
GetUsername(),
|
||||||
|
null,
|
||||||
|
cancellationToken);
|
||||||
|
if (access.Count == 0)
|
||||||
{
|
{
|
||||||
return Ok(new List<DenunciasGestiona>());
|
return Ok(new List<DenunciasGestiona>());
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(await _filteredDenunciaStore.GetDenunciasByIdsAsync(allowedIds, scope, cancellationToken));
|
var complaints = await _filteredDenunciaStore.GetDenunciasByIdsAsync(
|
||||||
|
access.Keys.ToArray(),
|
||||||
|
scope,
|
||||||
|
cancellationToken);
|
||||||
|
ApplyAccessMetadata(complaints, access);
|
||||||
|
return Ok(complaints);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{denunciaId:int}")]
|
[HttpGet("{denunciaId:int}")]
|
||||||
@@ -62,7 +71,18 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(await _denunciaStore.GetDenunciaByIdAsync(denunciaId, cancellationToken));
|
var complaint = await _denunciaStore.GetDenunciaByIdAsync(denunciaId, cancellationToken);
|
||||||
|
if (complaint is null)
|
||||||
|
{
|
||||||
|
return Ok(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var access = await _accessService.GetComplaintAccessAsync(
|
||||||
|
GetUsername(),
|
||||||
|
[denunciaId],
|
||||||
|
cancellationToken);
|
||||||
|
ApplyAccessMetadata([complaint], access);
|
||||||
|
return Ok(complaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
@@ -275,6 +295,24 @@ public sealed class DenunciasController : ControllerBase
|
|||||||
private string GetUsername()
|
private string GetUsername()
|
||||||
=> User.Identity?.Name ?? throw new InvalidOperationException("No hay usuario autenticado.");
|
=> User.Identity?.Name ?? throw new InvalidOperationException("No hay usuario autenticado.");
|
||||||
|
|
||||||
|
private static void ApplyAccessMetadata(
|
||||||
|
IEnumerable<DenunciasGestiona> complaints,
|
||||||
|
IReadOnlyDictionary<int, ComplaintAccessInfo> access)
|
||||||
|
{
|
||||||
|
foreach (var complaint in complaints)
|
||||||
|
{
|
||||||
|
if (!access.TryGetValue(complaint.Id_Denuncia, out var item))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
complaint.OwnerUsername = item.OwnerUsername;
|
||||||
|
complaint.OwnedByCurrentUser = item.OwnedByCurrentUser;
|
||||||
|
complaint.RequiresOwnerConfirmation = item.RequiresOwnerConfirmation;
|
||||||
|
complaint.OwnerWorkGroups = item.OwnerGroupCodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static GestionaComplaintFieldsResponse ToGestionaComplaintFields(DenunciasGestiona denuncia)
|
private static GestionaComplaintFieldsResponse ToGestionaComplaintFields(DenunciasGestiona denuncia)
|
||||||
{
|
{
|
||||||
var preferenciaNotificacion = ResolveNotificationPreference(denuncia);
|
var preferenciaNotificacion = ResolveNotificationPreference(denuncia);
|
||||||
|
|||||||
@@ -222,13 +222,15 @@ public sealed class InboxController : ControllerBase
|
|||||||
return Unauthorized(new ApiError("La sesion de GlobalLeaks ha caducado. Renueva el 2FA.", SessionExpired: true));
|
return Unauthorized(new ApiError("La sesion de GlobalLeaks ha caducado. Renueva el 2FA.", SessionExpired: true));
|
||||||
}
|
}
|
||||||
|
|
||||||
var report = string.IsNullOrWhiteSpace(request.Report.Id)
|
var report = request.Report with { Id = reportId };
|
||||||
? request.Report with { Id = reportId }
|
|
||||||
: request.Report;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _trackingService.EnsureReportCanBeImportedByUserAsync(username, report, cancellationToken);
|
await _trackingService.EnsureReportCanBeImportedByUserAsync(
|
||||||
|
username,
|
||||||
|
report,
|
||||||
|
request.ConfirmDifferentOwner,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
ReportDetailDto? reportDetail = null;
|
ReportDetailDto? reportDetail = null;
|
||||||
try
|
try
|
||||||
@@ -286,6 +288,10 @@ public sealed class InboxController : ControllerBase
|
|||||||
{
|
{
|
||||||
return ToGlobalLeaksApiError(ex, "importar la denuncia");
|
return ToGlobalLeaksApiError(ex, "importar la denuncia");
|
||||||
}
|
}
|
||||||
|
catch (ReportOwnershipException ex)
|
||||||
|
{
|
||||||
|
return Conflict(new ApiError(ex.Message));
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "No se ha podido importar la denuncia {ReportId} para {Username}.", reportId, username);
|
_logger.LogError(ex, "No se ha podido importar la denuncia {ReportId} para {Username}.", reportId, username);
|
||||||
|
|||||||
@@ -52,9 +52,20 @@ public sealed class TrackingController : ControllerBase
|
|||||||
TrackingImportPermissionRequest request,
|
TrackingImportPermissionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _trackingService.EnsureReportCanBeImportedByUserAsync(GetUsername(), request.Report, cancellationToken);
|
try
|
||||||
|
{
|
||||||
|
await _trackingService.EnsureReportCanBeImportedByUserAsync(
|
||||||
|
GetUsername(),
|
||||||
|
request.Report,
|
||||||
|
request.ConfirmDifferentOwner,
|
||||||
|
cancellationToken);
|
||||||
return Ok(new { ok = true });
|
return Ok(new { ok = true });
|
||||||
}
|
}
|
||||||
|
catch (ReportOwnershipException ex)
|
||||||
|
{
|
||||||
|
return Conflict(new ApiError(ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private string GetUsername()
|
private string GetUsername()
|
||||||
=> User.Identity?.Name ?? throw new InvalidOperationException("No hay usuario autenticado.");
|
=> User.Identity?.Name ?? throw new InvalidOperationException("No hay usuario autenticado.");
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ builder.Services.AddScoped<DenunciaInboxService>();
|
|||||||
builder.Services.AddScoped<GestionaDocumentWorkflowService>();
|
builder.Services.AddScoped<GestionaDocumentWorkflowService>();
|
||||||
builder.Services.AddScoped<GestionaExpedienteExceptionStore>();
|
builder.Services.AddScoped<GestionaExpedienteExceptionStore>();
|
||||||
builder.Services.AddScoped<UserComplaintAccessService>();
|
builder.Services.AddScoped<UserComplaintAccessService>();
|
||||||
|
builder.Services.AddScoped<WorkGroupAdministrationService>();
|
||||||
builder.Services.AddHttpClient<ManualPurgeService>();
|
builder.Services.AddHttpClient<ManualPurgeService>();
|
||||||
builder.Services.AddScoped<AppConfigurationService>();
|
builder.Services.AddScoped<AppConfigurationService>();
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,58 @@ CREATE TABLE IF NOT EXISTS app_users (
|
|||||||
UNIQUE KEY uq_app_users_username (username)
|
UNIQUE KEY uq_app_users_username (username)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS work_groups (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
code VARCHAR(32) NOT NULL,
|
||||||
|
name VARCHAR(256) NOT NULL,
|
||||||
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
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_work_groups_code (code)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
INSERT INTO work_groups (code, name)
|
||||||
|
VALUES
|
||||||
|
('600', 'Asuntos Juridicos y Proteccion a la Persona Denunciante'),
|
||||||
|
('510', 'SDI - Investigacion Entradas')
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
name = VALUES(name),
|
||||||
|
is_active = 1;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS app_user_groups (
|
||||||
|
app_user_id BIGINT NOT NULL,
|
||||||
|
work_group_id BIGINT NOT NULL,
|
||||||
|
assigned_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
assigned_by_username VARCHAR(256) NOT NULL DEFAULT '',
|
||||||
|
PRIMARY KEY (app_user_id, work_group_id),
|
||||||
|
KEY ix_app_user_groups_group (work_group_id),
|
||||||
|
CONSTRAINT fk_app_user_groups_user
|
||||||
|
FOREIGN KEY (app_user_id) REFERENCES app_users(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_app_user_groups_group
|
||||||
|
FOREIGN KEY (work_group_id) REFERENCES work_groups(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS app_user_group_history (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
app_user_id BIGINT NOT NULL,
|
||||||
|
work_group_id BIGINT NOT NULL,
|
||||||
|
action VARCHAR(16) NOT NULL,
|
||||||
|
changed_by_username VARCHAR(256) NOT NULL,
|
||||||
|
changed_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY ix_app_user_group_history_user (app_user_id),
|
||||||
|
KEY ix_app_user_group_history_group (work_group_id),
|
||||||
|
CONSTRAINT fk_app_user_group_history_user
|
||||||
|
FOREIGN KEY (app_user_id) REFERENCES app_users(id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_app_user_group_history_group
|
||||||
|
FOREIGN KEY (work_group_id) REFERENCES work_groups(id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS inbox_reports (
|
CREATE TABLE IF NOT EXISTS inbox_reports (
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
global_report_uuid CHAR(36) NOT NULL,
|
global_report_uuid CHAR(36) NOT NULL,
|
||||||
@@ -104,6 +156,7 @@ CREATE TABLE IF NOT EXISTS inbox_reports (
|
|||||||
last_seen_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE 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_at_utc DATETIME(6) NULL,
|
||||||
last_downloaded_by_user_id BIGINT NULL,
|
last_downloaded_by_user_id BIGINT NULL,
|
||||||
|
owner_user_id BIGINT NULL,
|
||||||
imported_complaint_report_id INT NULL,
|
imported_complaint_report_id INT NULL,
|
||||||
imported_to_store_at_utc DATETIME(6) NULL,
|
imported_to_store_at_utc DATETIME(6) NULL,
|
||||||
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
created_at_utc DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||||
@@ -112,8 +165,12 @@ CREATE TABLE IF NOT EXISTS inbox_reports (
|
|||||||
UNIQUE KEY uq_inbox_reports_uuid (global_report_uuid),
|
UNIQUE KEY uq_inbox_reports_uuid (global_report_uuid),
|
||||||
KEY ix_inbox_reports_progressive (progressive_id),
|
KEY ix_inbox_reports_progressive (progressive_id),
|
||||||
KEY ix_inbox_reports_downloaded (last_downloaded_at_utc),
|
KEY ix_inbox_reports_downloaded (last_downloaded_at_utc),
|
||||||
|
KEY ix_inbox_reports_owner (owner_user_id),
|
||||||
CONSTRAINT fk_inbox_reports_last_user
|
CONSTRAINT fk_inbox_reports_last_user
|
||||||
FOREIGN KEY (last_downloaded_by_user_id) REFERENCES app_users(id)
|
FOREIGN KEY (last_downloaded_by_user_id) REFERENCES app_users(id)
|
||||||
|
ON DELETE SET NULL,
|
||||||
|
CONSTRAINT fk_inbox_reports_owner_user
|
||||||
|
FOREIGN KEY (owner_user_id) REFERENCES app_users(id)
|
||||||
ON DELETE SET NULL
|
ON DELETE SET NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||||
|
|
||||||
|
|||||||
@@ -558,6 +558,14 @@ public sealed class GlobalLeaksClient
|
|||||||
reference,
|
reference,
|
||||||
defaultNewWhenNoReference: false,
|
defaultNewWhenNoReference: false,
|
||||||
"rfiles");
|
"rfiles");
|
||||||
|
var receivers = ParseReportReceivers(document.RootElement);
|
||||||
|
var receiverNames = BuildReceiverNameLookup(receivers);
|
||||||
|
comments = comments
|
||||||
|
.Select(comment => WithAuthorName(comment, receiverNames))
|
||||||
|
.ToArray();
|
||||||
|
receiverFiles = receiverFiles
|
||||||
|
.Select(file => WithAuthorName(file, receiverNames))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
var citizenCommentDates = comments
|
var citizenCommentDates = comments
|
||||||
.Where(comment => IsWhistleblowerActivityType(comment.Type))
|
.Where(comment => IsWhistleblowerActivityType(comment.Type))
|
||||||
@@ -590,7 +598,24 @@ public sealed class GlobalLeaksClient
|
|||||||
: citizenCommentDates.Concat(citizenFileDates).Append(reportCreationDate.Value);
|
: citizenCommentDates.Concat(citizenFileDates).Append(reportCreationDate.Value);
|
||||||
|
|
||||||
var citizenLast = MaxDate(citizenActivityDates);
|
var citizenLast = MaxDate(citizenActivityDates);
|
||||||
var receiverLast = MaxDate(receiverCommentDates.Concat(receiverFileDates).Concat(unclassifiedCommentDates));
|
var receiverEvents = comments
|
||||||
|
.Where(comment =>
|
||||||
|
IsReceiverActivityType(comment.Type) ||
|
||||||
|
IsUnclassifiedActivityType(comment.Type))
|
||||||
|
.Select(comment => new ActivityActorEvent(
|
||||||
|
ParseDate(comment.CreationDate),
|
||||||
|
comment.AuthorId,
|
||||||
|
comment.AuthorName))
|
||||||
|
.Concat(receiverFiles.Select(file => new ActivityActorEvent(
|
||||||
|
ParseDate(file.CreationDate),
|
||||||
|
file.AuthorId,
|
||||||
|
file.AuthorName)))
|
||||||
|
.Where(item => item.Date is not null)
|
||||||
|
.OrderByDescending(item => item.Date)
|
||||||
|
.ToArray();
|
||||||
|
var latestReceiverEvent = receiverEvents.FirstOrDefault();
|
||||||
|
var receiverLast = latestReceiverEvent?.Date ??
|
||||||
|
MaxDate(receiverCommentDates.Concat(receiverFileDates).Concat(unclassifiedCommentDates));
|
||||||
|
|
||||||
return new ReportActivitySnapshot(
|
return new ReportActivitySnapshot(
|
||||||
citizenLast,
|
citizenLast,
|
||||||
@@ -605,7 +630,9 @@ public sealed class GlobalLeaksClient
|
|||||||
comments.Any(comment =>
|
comments.Any(comment =>
|
||||||
IsUnclassifiedActivityType(comment.Type) &&
|
IsUnclassifiedActivityType(comment.Type) &&
|
||||||
comment.IsNew) ||
|
comment.IsNew) ||
|
||||||
receiverFiles.Any(file => file.IsNew));
|
receiverFiles.Any(file => file.IsNew),
|
||||||
|
latestReceiverEvent?.AuthorId,
|
||||||
|
latestReceiverEvent?.AuthorName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ReportDto ApplyActivity(ReportDto report, ReportActivitySnapshot activity)
|
private static ReportDto ApplyActivity(ReportDto report, ReportActivitySnapshot activity)
|
||||||
@@ -618,7 +645,9 @@ public sealed class GlobalLeaksClient
|
|||||||
CitizenHasNewComment = activity.HasNewCitizenComment,
|
CitizenHasNewComment = activity.HasNewCitizenComment,
|
||||||
CitizenHasNewFile = activity.HasNewCitizenFile,
|
CitizenHasNewFile = activity.HasNewCitizenFile,
|
||||||
ReceiverLastActivity = activity.ReceiverLastActivity?.ToString("O", CultureInfo.InvariantCulture),
|
ReceiverLastActivity = activity.ReceiverLastActivity?.ToString("O", CultureInfo.InvariantCulture),
|
||||||
ReceiverHasNewActivity = activity.HasNewReceiverActivity
|
ReceiverHasNewActivity = activity.HasNewReceiverActivity,
|
||||||
|
ReceiverLastActivityAuthorId = activity.ReceiverLastActivityAuthorId,
|
||||||
|
ReceiverLastActivityAuthorName = activity.ReceiverLastActivityAuthorName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1043,10 +1072,32 @@ public sealed class GlobalLeaksClient
|
|||||||
.Select(item => CreateReportComment(item, lastAccessDate, defaultNewWhenNoReference: true))
|
.Select(item => CreateReportComment(item, lastAccessDate, defaultNewWhenNoReference: true))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var whistleblowerFiles = ParseReportFiles(root, lastAccessDate, defaultNewWhenNoReference: true, "wbfiles", "files");
|
var receivers = ParseReportReceivers(root);
|
||||||
var receiverFiles = ParseReportFiles(root, lastAccessDate, defaultNewWhenNoReference: true, "rfiles");
|
var receiverNames = BuildReceiverNameLookup(receivers);
|
||||||
|
comments = comments
|
||||||
|
.Select(comment => WithAuthorName(comment, receiverNames))
|
||||||
|
.ToArray();
|
||||||
|
var whistleblowerFiles = ParseReportFiles(
|
||||||
|
root,
|
||||||
|
lastAccessDate,
|
||||||
|
defaultNewWhenNoReference: true,
|
||||||
|
"wbfiles",
|
||||||
|
"files");
|
||||||
|
var receiverFiles = ParseReportFiles(
|
||||||
|
root,
|
||||||
|
lastAccessDate,
|
||||||
|
defaultNewWhenNoReference: true,
|
||||||
|
"rfiles")
|
||||||
|
.Select(file => WithAuthorName(file, receiverNames))
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
return new ReportDetailDto(reportId, lastAccess, comments, whistleblowerFiles, receiverFiles);
|
return new ReportDetailDto(
|
||||||
|
reportId,
|
||||||
|
lastAccess,
|
||||||
|
comments,
|
||||||
|
whistleblowerFiles,
|
||||||
|
receiverFiles,
|
||||||
|
receivers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ReportCommentDto CreateReportComment(
|
private static ReportCommentDto CreateReportComment(
|
||||||
@@ -1055,12 +1106,25 @@ public sealed class GlobalLeaksClient
|
|||||||
bool defaultNewWhenNoReference)
|
bool defaultNewWhenNoReference)
|
||||||
{
|
{
|
||||||
var creationDate = GetString(item, "creation_date", "creationDate", "date", "created_at", "createdAt");
|
var creationDate = GetString(item, "creation_date", "creationDate", "date", "created_at", "createdAt");
|
||||||
|
var authorId = GetString(item, "author_id", "authorId");
|
||||||
|
var activityType = GetCommentActivityType(item);
|
||||||
|
if (!string.IsNullOrWhiteSpace(authorId))
|
||||||
|
{
|
||||||
|
activityType = "receiver";
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(activityType) ||
|
||||||
|
IsUnclassifiedActivityType(activityType))
|
||||||
|
{
|
||||||
|
activityType = "whistleblower";
|
||||||
|
}
|
||||||
|
|
||||||
return new ReportCommentDto(
|
return new ReportCommentDto(
|
||||||
GetString(item, "id"),
|
GetString(item, "id"),
|
||||||
GetCommentActivityType(item),
|
activityType,
|
||||||
GetString(item, "content", "text", "message"),
|
GetString(item, "content", "text", "message"),
|
||||||
creationDate,
|
creationDate,
|
||||||
IsAfterReference(creationDate, reference, defaultNewWhenNoReference));
|
IsAfterReference(creationDate, reference, defaultNewWhenNoReference),
|
||||||
|
authorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? GetCommentActivityType(JsonElement item)
|
private static string? GetCommentActivityType(JsonElement item)
|
||||||
@@ -1196,13 +1260,62 @@ public sealed class GlobalLeaksClient
|
|||||||
GetString(item, "id"),
|
GetString(item, "id"),
|
||||||
GetLocalizedString(item, "name", "file_name", "filename"),
|
GetLocalizedString(item, "name", "file_name", "filename"),
|
||||||
GetInt64(item, "size"),
|
GetInt64(item, "size"),
|
||||||
GetString(item, "content_type", "contentType", "mime_type", "mimetype"),
|
GetString(item, "content_type", "contentType", "mime_type", "mimetype", "type"),
|
||||||
creationDate,
|
creationDate,
|
||||||
IsAfterReference(creationDate, reference, defaultNewWhenNoReference));
|
IsAfterReference(creationDate, reference, defaultNewWhenNoReference),
|
||||||
|
GetString(item, "author_id", "authorId"));
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ReportReceiverDto[] ParseReportReceivers(JsonElement root)
|
||||||
|
{
|
||||||
|
return EnumerateArray(root, "receivers", "recipients")
|
||||||
|
.Select(item => new ReportReceiverDto(
|
||||||
|
GetString(item, "id") ?? string.Empty,
|
||||||
|
GetLocalizedString(item, "name", "display_name", "displayName", "username") ?? "Gestor",
|
||||||
|
GetBool(item, "active", "is_active", "isActive")))
|
||||||
|
.Where(receiver => !string.IsNullOrWhiteSpace(receiver.Id))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IReadOnlyDictionary<string, string> BuildReceiverNameLookup(
|
||||||
|
IEnumerable<ReportReceiverDto> receivers)
|
||||||
|
{
|
||||||
|
return receivers
|
||||||
|
.GroupBy(receiver => receiver.Id, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(
|
||||||
|
group => group.Key,
|
||||||
|
group => group.First().Name,
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReportCommentDto WithAuthorName(
|
||||||
|
ReportCommentDto comment,
|
||||||
|
IReadOnlyDictionary<string, string> receiverNames)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(comment.AuthorId) ||
|
||||||
|
!receiverNames.TryGetValue(comment.AuthorId, out var authorName))
|
||||||
|
{
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment with { AuthorName = authorName };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReportFileDto WithAuthorName(
|
||||||
|
ReportFileDto file,
|
||||||
|
IReadOnlyDictionary<string, string> receiverNames)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(file.AuthorId) ||
|
||||||
|
!receiverNames.TryGetValue(file.AuthorId, out var authorName))
|
||||||
|
{
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
return file with { AuthorName = authorName };
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsAfterReference(
|
private static bool IsAfterReference(
|
||||||
string? value,
|
string? value,
|
||||||
DateTimeOffset? reference,
|
DateTimeOffset? reference,
|
||||||
@@ -1482,7 +1595,14 @@ public sealed class GlobalLeaksClient
|
|||||||
bool HasNewCitizenComment,
|
bool HasNewCitizenComment,
|
||||||
bool HasNewCitizenFile,
|
bool HasNewCitizenFile,
|
||||||
DateTimeOffset? ReceiverLastActivity,
|
DateTimeOffset? ReceiverLastActivity,
|
||||||
bool HasNewReceiverActivity);
|
bool HasNewReceiverActivity,
|
||||||
|
string? ReceiverLastActivityAuthorId,
|
||||||
|
string? ReceiverLastActivityAuthorName);
|
||||||
|
|
||||||
|
private sealed record ActivityActorEvent(
|
||||||
|
DateTimeOffset? Date,
|
||||||
|
string? AuthorId,
|
||||||
|
string? AuthorName);
|
||||||
|
|
||||||
private sealed record RawReport
|
private sealed record RawReport
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -104,16 +104,25 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
|||||||
LastDownloadedAt = meta?.LastDownloadedAtUtc?.ToString("O", CultureInfo.InvariantCulture),
|
LastDownloadedAt = meta?.LastDownloadedAtUtc?.ToString("O", CultureInfo.InvariantCulture),
|
||||||
AlreadyImported = meta?.AlreadyImported ?? false,
|
AlreadyImported = meta?.AlreadyImported ?? false,
|
||||||
AlreadyInGestiona = meta?.AlreadyInGestiona ?? false,
|
AlreadyInGestiona = meta?.AlreadyInGestiona ?? false,
|
||||||
|
OwnerUsername = meta?.OwnerUsername,
|
||||||
|
OwnedByCurrentUser = meta?.OwnedByCurrentUser ?? false,
|
||||||
|
AccessibleByWorkGroup = meta?.AccessibleByWorkGroup ?? false,
|
||||||
|
RequiresOwnerConfirmation = meta?.RequiresOwnerConfirmation ?? false,
|
||||||
|
OwnerWorkGroups = meta?.OwnerWorkGroups ?? [],
|
||||||
TrackingNote = BuildTrackingNote(meta)
|
TrackingNote = BuildTrackingNote(meta)
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.Where(report => !IsLockedByAnotherUser(report))
|
.Where(report =>
|
||||||
|
string.IsNullOrWhiteSpace(report.OwnerUsername) ||
|
||||||
|
report.OwnedByCurrentUser ||
|
||||||
|
report.AccessibleByWorkGroup)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task EnsureReportCanBeImportedByUserAsync(
|
public async Task EnsureReportCanBeImportedByUserAsync(
|
||||||
string username,
|
string username,
|
||||||
ReportDto report,
|
ReportDto report,
|
||||||
|
bool confirmDifferentOwner = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||||
@@ -140,14 +149,23 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
|||||||
|
|
||||||
await EnsureConnectionOpenAsync(connection, cancellationToken);
|
await EnsureConnectionOpenAsync(connection, cancellationToken);
|
||||||
var metadata = await LoadMetadataAsync(connection, userId, [report.Id], cancellationToken);
|
var metadata = await LoadMetadataAsync(connection, userId, [report.Id], cancellationToken);
|
||||||
if (metadata.TryGetValue(report.Id, out var meta) && meta.LockedByAnotherUser)
|
if (!metadata.TryGetValue(report.Id, out var meta) ||
|
||||||
|
string.IsNullOrWhiteSpace(meta.OwnerUsername) ||
|
||||||
|
meta.OwnedByCurrentUser)
|
||||||
{
|
{
|
||||||
var owner = string.IsNullOrWhiteSpace(meta.LastDownloadedByUsername)
|
return;
|
||||||
? "otro usuario"
|
}
|
||||||
: meta.LastDownloadedByUsername;
|
|
||||||
|
|
||||||
throw new InvalidOperationException(
|
if (!meta.AccessibleByWorkGroup)
|
||||||
$"La denuncia ya fue importada por {owner}. Solo ese usuario puede ver e importar sus actualizaciones.");
|
{
|
||||||
|
throw new ReportOwnershipException(
|
||||||
|
$"La denuncia pertenece a {meta.OwnerUsername} y no compartis ningun grupo de trabajo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirmDifferentOwner)
|
||||||
|
{
|
||||||
|
throw new ReportOwnershipException(
|
||||||
|
$"La denuncia pertenece a {meta.OwnerUsername}. Confirma expresamente que deseas importarla como miembro de su grupo.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,6 +198,7 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
|||||||
SET
|
SET
|
||||||
last_downloaded_at_utc = @nowUtc,
|
last_downloaded_at_utc = @nowUtc,
|
||||||
last_downloaded_by_user_id = @userId,
|
last_downloaded_by_user_id = @userId,
|
||||||
|
owner_user_id = COALESCE(owner_user_id, @userId),
|
||||||
imported_complaint_report_id = COALESCE(@complaintId, imported_complaint_report_id),
|
imported_complaint_report_id = COALESCE(@complaintId, imported_complaint_report_id),
|
||||||
imported_to_store_at_utc = COALESCE(imported_to_store_at_utc, @nowUtc),
|
imported_to_store_at_utc = COALESCE(imported_to_store_at_utc, @nowUtc),
|
||||||
updated_at_utc = CURRENT_TIMESTAMP(6)
|
updated_at_utc = CURRENT_TIMESTAMP(6)
|
||||||
@@ -298,6 +317,7 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
|||||||
ELSE last_downloaded_at_utc
|
ELSE last_downloaded_at_utc
|
||||||
END,
|
END,
|
||||||
last_downloaded_by_user_id = @userId,
|
last_downloaded_by_user_id = @userId,
|
||||||
|
owner_user_id = COALESCE(owner_user_id, @userId),
|
||||||
imported_complaint_report_id = COALESCE(imported_complaint_report_id, @denunciaId),
|
imported_complaint_report_id = COALESCE(imported_complaint_report_id, @denunciaId),
|
||||||
imported_to_store_at_utc = COALESCE(imported_to_store_at_utc, @handledAtUtc),
|
imported_to_store_at_utc = COALESCE(imported_to_store_at_utc, @handledAtUtc),
|
||||||
updated_at_utc = CURRENT_TIMESTAMP(6)
|
updated_at_utc = CURRENT_TIMESTAMP(6)
|
||||||
@@ -512,11 +532,37 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
|||||||
ir.global_report_uuid,
|
ir.global_report_uuid,
|
||||||
ir.last_downloaded_at_utc,
|
ir.last_downloaded_at_utc,
|
||||||
downloader.username AS last_downloaded_by_username,
|
downloader.username AS last_downloaded_by_username,
|
||||||
|
owner.username AS owner_username,
|
||||||
ir.imported_to_store_at_utc,
|
ir.imported_to_store_at_utc,
|
||||||
COALESCE(c.is_in_gestiona, 0) AS already_in_gestiona,
|
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
|
CASE WHEN uir.last_downloaded_at_utc IS NULL THEN 0 ELSE 1 END AS downloaded_by_current_user,
|
||||||
|
CASE WHEN ir.owner_user_id = @userId THEN 1 ELSE 0 END AS owned_by_current_user,
|
||||||
|
CASE
|
||||||
|
WHEN ir.owner_user_id IS NULL THEN 0
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM app_user_groups current_group
|
||||||
|
INNER JOIN app_user_groups owner_group
|
||||||
|
ON owner_group.work_group_id = current_group.work_group_id
|
||||||
|
INNER JOIN work_groups active_group
|
||||||
|
ON active_group.id = current_group.work_group_id
|
||||||
|
AND active_group.is_active = 1
|
||||||
|
WHERE current_group.app_user_id = @userId
|
||||||
|
AND owner_group.app_user_id = ir.owner_user_id
|
||||||
|
) THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS accessible_by_group,
|
||||||
|
(
|
||||||
|
SELECT GROUP_CONCAT(DISTINCT wg.code ORDER BY wg.code SEPARATOR ',')
|
||||||
|
FROM app_user_groups owner_membership
|
||||||
|
INNER JOIN work_groups wg
|
||||||
|
ON wg.id = owner_membership.work_group_id
|
||||||
|
AND wg.is_active = 1
|
||||||
|
WHERE owner_membership.app_user_id = ir.owner_user_id
|
||||||
|
) AS owner_group_codes
|
||||||
FROM inbox_reports ir
|
FROM inbox_reports ir
|
||||||
LEFT JOIN app_users downloader ON downloader.id = ir.last_downloaded_by_user_id
|
LEFT JOIN app_users downloader ON downloader.id = ir.last_downloaded_by_user_id
|
||||||
|
LEFT JOIN app_users owner ON owner.id = ir.owner_user_id
|
||||||
LEFT JOIN user_inbox_reports uir
|
LEFT JOIN user_inbox_reports uir
|
||||||
ON uir.inbox_report_id = ir.id
|
ON uir.inbox_report_id = ir.id
|
||||||
AND uir.app_user_id = @userId
|
AND uir.app_user_id = @userId
|
||||||
@@ -533,14 +579,20 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
|||||||
? null
|
? null
|
||||||
: reader.GetString(reader.GetOrdinal("last_downloaded_by_username"));
|
: reader.GetString(reader.GetOrdinal("last_downloaded_by_username"));
|
||||||
var downloadedByCurrentUser = reader.GetInt32(reader.GetOrdinal("downloaded_by_current_user")) == 1;
|
var downloadedByCurrentUser = reader.GetInt32(reader.GetOrdinal("downloaded_by_current_user")) == 1;
|
||||||
var lockedByAnotherUser =
|
|
||||||
!downloadedByCurrentUser &&
|
|
||||||
!reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")) &&
|
|
||||||
!string.IsNullOrWhiteSpace(lastDownloadedByUsername);
|
|
||||||
|
|
||||||
var downloadedByAnotherUser =
|
var downloadedByAnotherUser =
|
||||||
!downloadedByCurrentUser &&
|
!downloadedByCurrentUser &&
|
||||||
!string.IsNullOrWhiteSpace(lastDownloadedByUsername);
|
!string.IsNullOrWhiteSpace(lastDownloadedByUsername);
|
||||||
|
var ownerUsername = reader.IsDBNull(reader.GetOrdinal("owner_username"))
|
||||||
|
? null
|
||||||
|
: reader.GetString(reader.GetOrdinal("owner_username"));
|
||||||
|
var ownedByCurrentUser =
|
||||||
|
reader.GetInt32(reader.GetOrdinal("owned_by_current_user")) == 1;
|
||||||
|
var accessibleByGroup =
|
||||||
|
reader.GetInt32(reader.GetOrdinal("accessible_by_group")) == 1;
|
||||||
|
var ownerWorkGroups = reader.IsDBNull(reader.GetOrdinal("owner_group_codes"))
|
||||||
|
? []
|
||||||
|
: reader.GetString(reader.GetOrdinal("owner_group_codes"))
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
metadata[reportId] = new ReportMetadata
|
metadata[reportId] = new ReportMetadata
|
||||||
{
|
{
|
||||||
@@ -550,7 +602,10 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
|||||||
LastDownloadedAtUtc = GetDateTimeOffset(reader, "last_downloaded_at_utc"),
|
LastDownloadedAtUtc = GetDateTimeOffset(reader, "last_downloaded_at_utc"),
|
||||||
AlreadyImported = !reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")),
|
AlreadyImported = !reader.IsDBNull(reader.GetOrdinal("imported_to_store_at_utc")),
|
||||||
AlreadyInGestiona = reader.GetInt32(reader.GetOrdinal("already_in_gestiona")) == 1,
|
AlreadyInGestiona = reader.GetInt32(reader.GetOrdinal("already_in_gestiona")) == 1,
|
||||||
LockedByAnotherUser = lockedByAnotherUser,
|
OwnerUsername = ownerUsername,
|
||||||
|
OwnedByCurrentUser = ownedByCurrentUser,
|
||||||
|
AccessibleByWorkGroup = accessibleByGroup,
|
||||||
|
OwnerWorkGroups = ownerWorkGroups,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,11 +709,10 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata.LockedByAnotherUser)
|
if (!string.IsNullOrWhiteSpace(metadata.OwnerUsername) &&
|
||||||
|
!metadata.OwnedByCurrentUser)
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(metadata.LastDownloadedByUsername)
|
return $"Propiedad de {metadata.OwnerUsername}";
|
||||||
? "Importada por otro usuario"
|
|
||||||
: $"Importada por {metadata.LastDownloadedByUsername}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata.AlreadyInGestiona)
|
if (metadata.AlreadyInGestiona)
|
||||||
@@ -693,15 +747,25 @@ public sealed class InboxTrackingService : IInboxTrackingService
|
|||||||
{
|
{
|
||||||
public bool DownloadedByCurrentUser { get; init; }
|
public bool DownloadedByCurrentUser { get; init; }
|
||||||
public bool DownloadedByAnotherUser { get; init; }
|
public bool DownloadedByAnotherUser { get; init; }
|
||||||
public bool LockedByAnotherUser { get; init; }
|
|
||||||
public string? LastDownloadedByUsername { get; init; }
|
public string? LastDownloadedByUsername { get; init; }
|
||||||
public DateTimeOffset? LastDownloadedAtUtc { get; init; }
|
public DateTimeOffset? LastDownloadedAtUtc { get; init; }
|
||||||
public bool AlreadyImported { get; init; }
|
public bool AlreadyImported { get; init; }
|
||||||
public bool AlreadyInGestiona { get; init; }
|
public bool AlreadyInGestiona { get; init; }
|
||||||
|
public string? OwnerUsername { get; init; }
|
||||||
|
public bool OwnedByCurrentUser { get; init; }
|
||||||
|
public bool AccessibleByWorkGroup { get; init; }
|
||||||
|
public IReadOnlyList<string> OwnerWorkGroups { get; init; } = [];
|
||||||
|
public bool RequiresOwnerConfirmation =>
|
||||||
|
!OwnedByCurrentUser &&
|
||||||
|
AccessibleByWorkGroup &&
|
||||||
|
!string.IsNullOrWhiteSpace(OwnerUsername);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsLockedByAnotherUser(ReportDto report)
|
public sealed class ReportOwnershipException : InvalidOperationException
|
||||||
=> report.AlreadyImported &&
|
{
|
||||||
report.DownloadedByAnotherUser &&
|
public ReportOwnershipException(string message)
|
||||||
!report.DownloadedByCurrentUser;
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
("complaints", "encrypted_at_utc", "`encrypted_at_utc` DATETIME(6) NULL"),
|
("complaints", "encrypted_at_utc", "`encrypted_at_utc` DATETIME(6) NULL"),
|
||||||
("complaints", "gestiona_last_upload_type", "`gestiona_last_upload_type` VARCHAR(64) NOT NULL DEFAULT ''"),
|
("complaints", "gestiona_last_upload_type", "`gestiona_last_upload_type` VARCHAR(64) NOT NULL DEFAULT ''"),
|
||||||
("complaints", "gestiona_assigned_group", "`gestiona_assigned_group` VARCHAR(255) NOT NULL DEFAULT ''"),
|
("complaints", "gestiona_assigned_group", "`gestiona_assigned_group` VARCHAR(255) NOT NULL DEFAULT ''"),
|
||||||
|
("inbox_reports", "owner_user_id", "`owner_user_id` BIGINT NULL"),
|
||||||
("complaint_attachments", "content_sha256", "`content_sha256` CHAR(64) NOT NULL DEFAULT ''"),
|
("complaint_attachments", "content_sha256", "`content_sha256` CHAR(64) NOT NULL DEFAULT ''"),
|
||||||
("complaint_attachments", "key_date", "`key_date` DATE NULL"),
|
("complaint_attachments", "key_date", "`key_date` DATE NULL"),
|
||||||
("complaint_attachments", "encryption_scheme", "`encryption_scheme` VARCHAR(64) NOT NULL DEFAULT 'none'"),
|
("complaint_attachments", "encryption_scheme", "`encryption_scheme` VARCHAR(64) NOT NULL DEFAULT 'none'"),
|
||||||
@@ -195,6 +196,7 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
("complaint_attachments", "ix_attachments_sha256", "INDEX `ix_attachments_sha256` (`content_sha256`)"),
|
("complaint_attachments", "ix_attachments_sha256", "INDEX `ix_attachments_sha256` (`content_sha256`)"),
|
||||||
("complaints", "ix_complaints_key_date", "INDEX `ix_complaints_key_date` (`key_date`)"),
|
("complaints", "ix_complaints_key_date", "INDEX `ix_complaints_key_date` (`key_date`)"),
|
||||||
("complaints", "ix_complaints_flags", "INDEX `ix_complaints_flags` (`is_update`, `is_in_gestiona`, `is_rejected`)"),
|
("complaints", "ix_complaints_flags", "INDEX `ix_complaints_flags` (`is_update`, `is_in_gestiona`, `is_rejected`)"),
|
||||||
|
("inbox_reports", "ix_inbox_reports_owner", "INDEX `ix_inbox_reports_owner` (`owner_user_id`)"),
|
||||||
("complaint_attachments", "ix_attachments_key_date", "INDEX `ix_attachments_key_date` (`key_date`)"),
|
("complaint_attachments", "ix_attachments_key_date", "INDEX `ix_attachments_key_date` (`key_date`)"),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -1354,6 +1356,54 @@ public sealed class MySqlDenunciaStore : IDenunciaStore
|
|||||||
await alterCommand.ExecuteNonQueryAsync(cancellationToken);
|
await alterCommand.ExecuteNonQueryAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await BackfillReportOwnersAsync(connection, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task BackfillReportOwnersAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
UPDATE inbox_reports ir
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
uir.inbox_report_id,
|
||||||
|
CAST(
|
||||||
|
SUBSTRING_INDEX(
|
||||||
|
GROUP_CONCAT(
|
||||||
|
uir.app_user_id
|
||||||
|
ORDER BY
|
||||||
|
COALESCE(
|
||||||
|
uir.first_downloaded_at_utc,
|
||||||
|
uir.last_downloaded_at_utc,
|
||||||
|
uir.first_seen_at_utc
|
||||||
|
),
|
||||||
|
uir.app_user_id
|
||||||
|
SEPARATOR ','
|
||||||
|
),
|
||||||
|
',',
|
||||||
|
1
|
||||||
|
) AS UNSIGNED
|
||||||
|
) AS first_owner_user_id
|
||||||
|
FROM user_inbox_reports uir
|
||||||
|
WHERE uir.first_downloaded_at_utc IS NOT NULL
|
||||||
|
OR uir.last_downloaded_at_utc IS NOT NULL
|
||||||
|
GROUP BY uir.inbox_report_id
|
||||||
|
) first_owner ON first_owner.inbox_report_id = ir.id
|
||||||
|
SET ir.owner_user_id = COALESCE(
|
||||||
|
first_owner.first_owner_user_id,
|
||||||
|
ir.last_downloaded_by_user_id
|
||||||
|
)
|
||||||
|
WHERE ir.owner_user_id IS NULL
|
||||||
|
AND (
|
||||||
|
ir.imported_to_store_at_utc IS NOT NULL
|
||||||
|
OR ir.imported_complaint_report_id IS NOT NULL
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var command = new MySqlCommand(sql, connection);
|
||||||
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task EnsureAttachmentChunksTableAsync(
|
private static async Task EnsureAttachmentChunksTableAsync(
|
||||||
|
|||||||
@@ -12,43 +12,165 @@ public sealed class UserComplaintAccessService
|
|||||||
_connectionStringProvider = connectionStringProvider;
|
_connectionStringProvider = connectionStringProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HashSet<int>> GetAllowedComplaintIdsAsync(string username, CancellationToken cancellationToken = default)
|
public async Task<HashSet<int>> GetAllowedComplaintIdsAsync(
|
||||||
|
string username,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var access = await GetComplaintAccessAsync(username, null, cancellationToken);
|
||||||
|
return access.Keys.ToHashSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyDictionary<int, ComplaintAccessInfo>> GetComplaintAccessAsync(
|
||||||
|
string username,
|
||||||
|
IReadOnlyCollection<int>? complaintIds = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(username))
|
if (string.IsNullOrWhiteSpace(username))
|
||||||
{
|
{
|
||||||
return [];
|
return new Dictionary<int, ComplaintAccessInfo>();
|
||||||
}
|
}
|
||||||
|
|
||||||
const string sql = """
|
|
||||||
SELECT DISTINCT ir.imported_complaint_report_id
|
|
||||||
FROM inbox_reports ir
|
|
||||||
INNER JOIN user_inbox_reports uir ON uir.inbox_report_id = ir.id
|
|
||||||
INNER JOIN app_users au ON au.id = uir.app_user_id
|
|
||||||
WHERE au.username = @username
|
|
||||||
AND ir.imported_complaint_report_id IS NOT NULL
|
|
||||||
AND uir.download_count > 0;
|
|
||||||
""";
|
|
||||||
|
|
||||||
var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
|
var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
|
||||||
await using var connection = new MySqlConnection(connectionString);
|
await using var connection = new MySqlConnection(connectionString);
|
||||||
await connection.OpenAsync(cancellationToken);
|
await connection.OpenAsync(cancellationToken);
|
||||||
|
|
||||||
await using var command = new MySqlCommand(sql, connection);
|
await using var command = connection.CreateCommand();
|
||||||
command.Parameters.AddWithValue("@username", username.Trim());
|
command.Parameters.AddWithValue("@username", username.Trim());
|
||||||
|
|
||||||
var result = new HashSet<int>();
|
var idFilter = string.Empty;
|
||||||
|
if (complaintIds is { Count: > 0 })
|
||||||
|
{
|
||||||
|
var parameters = new List<string>(complaintIds.Count);
|
||||||
|
var index = 0;
|
||||||
|
foreach (var complaintId in complaintIds.Where(id => id > 0).Distinct())
|
||||||
|
{
|
||||||
|
var parameterName = $"@complaintId{index++}";
|
||||||
|
parameters.Add(parameterName);
|
||||||
|
command.Parameters.AddWithValue(parameterName, complaintId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameters.Count == 0)
|
||||||
|
{
|
||||||
|
return new Dictionary<int, ComplaintAccessInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
idFilter = $"AND COALESCE(ir.imported_complaint_report_id, ir.progressive_id) IN ({string.Join(", ", parameters)})";
|
||||||
|
}
|
||||||
|
|
||||||
|
command.CommandText = $"""
|
||||||
|
SELECT
|
||||||
|
COALESCE(ir.imported_complaint_report_id, ir.progressive_id) AS complaint_id,
|
||||||
|
owner.username AS owner_username,
|
||||||
|
CASE WHEN ir.owner_user_id = current_user.id THEN 1 ELSE 0 END AS owned_by_current_user,
|
||||||
|
CASE
|
||||||
|
WHEN ir.owner_user_id IS NULL THEN 0
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM app_user_groups current_group
|
||||||
|
INNER JOIN app_user_groups owner_group
|
||||||
|
ON owner_group.work_group_id = current_group.work_group_id
|
||||||
|
INNER JOIN work_groups active_group
|
||||||
|
ON active_group.id = current_group.work_group_id
|
||||||
|
AND active_group.is_active = 1
|
||||||
|
WHERE current_group.app_user_id = current_user.id
|
||||||
|
AND owner_group.app_user_id = ir.owner_user_id
|
||||||
|
) THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END AS accessible_by_group,
|
||||||
|
(
|
||||||
|
SELECT GROUP_CONCAT(DISTINCT wg.code ORDER BY wg.code SEPARATOR ',')
|
||||||
|
FROM app_user_groups owner_membership
|
||||||
|
INNER JOIN work_groups wg
|
||||||
|
ON wg.id = owner_membership.work_group_id
|
||||||
|
AND wg.is_active = 1
|
||||||
|
WHERE owner_membership.app_user_id = ir.owner_user_id
|
||||||
|
) AS owner_group_codes
|
||||||
|
FROM app_users current_user
|
||||||
|
INNER JOIN inbox_reports ir
|
||||||
|
ON COALESCE(ir.imported_complaint_report_id, ir.progressive_id) IS NOT NULL
|
||||||
|
LEFT JOIN app_users owner ON owner.id = ir.owner_user_id
|
||||||
|
LEFT JOIN user_inbox_reports current_tracking
|
||||||
|
ON current_tracking.inbox_report_id = ir.id
|
||||||
|
AND current_tracking.app_user_id = current_user.id
|
||||||
|
WHERE current_user.username = @username
|
||||||
|
{idFilter}
|
||||||
|
AND (
|
||||||
|
ir.owner_user_id = current_user.id
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM app_user_groups current_group
|
||||||
|
INNER JOIN app_user_groups owner_group
|
||||||
|
ON owner_group.work_group_id = current_group.work_group_id
|
||||||
|
INNER JOIN work_groups active_group
|
||||||
|
ON active_group.id = current_group.work_group_id
|
||||||
|
AND active_group.is_active = 1
|
||||||
|
WHERE current_group.app_user_id = current_user.id
|
||||||
|
AND owner_group.app_user_id = ir.owner_user_id
|
||||||
|
)
|
||||||
|
OR (
|
||||||
|
ir.owner_user_id IS NULL
|
||||||
|
AND current_tracking.download_count > 0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
|
||||||
|
var result = new Dictionary<int, ComplaintAccessInfo>();
|
||||||
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||||
while (await reader.ReadAsync(cancellationToken))
|
while (await reader.ReadAsync(cancellationToken))
|
||||||
{
|
{
|
||||||
result.Add(Convert.ToInt32(reader.GetValue(0), CultureInfo.InvariantCulture));
|
var complaintId = Convert.ToInt32(
|
||||||
|
reader.GetValue(reader.GetOrdinal("complaint_id")),
|
||||||
|
CultureInfo.InvariantCulture);
|
||||||
|
var ownerUsername = reader.IsDBNull(reader.GetOrdinal("owner_username"))
|
||||||
|
? string.Empty
|
||||||
|
: reader.GetString(reader.GetOrdinal("owner_username"));
|
||||||
|
var ownedByCurrentUser =
|
||||||
|
reader.GetInt32(reader.GetOrdinal("owned_by_current_user")) == 1;
|
||||||
|
var accessibleByGroup =
|
||||||
|
reader.GetInt32(reader.GetOrdinal("accessible_by_group")) == 1;
|
||||||
|
var groupCodes = reader.IsDBNull(reader.GetOrdinal("owner_group_codes"))
|
||||||
|
? []
|
||||||
|
: reader.GetString(reader.GetOrdinal("owner_group_codes"))
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
|
||||||
|
result[complaintId] = new ComplaintAccessInfo(
|
||||||
|
complaintId,
|
||||||
|
ownerUsername,
|
||||||
|
ownedByCurrentUser,
|
||||||
|
accessibleByGroup,
|
||||||
|
groupCodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CanAccessComplaintAsync(string username, int complaintId, CancellationToken cancellationToken = default)
|
public async Task<bool> CanAccessComplaintAsync(
|
||||||
|
string username,
|
||||||
|
int complaintId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var allowedIds = await GetAllowedComplaintIdsAsync(username, cancellationToken);
|
if (complaintId <= 0)
|
||||||
return allowedIds.Contains(complaintId);
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var access = await GetComplaintAccessAsync(
|
||||||
|
username,
|
||||||
|
[complaintId],
|
||||||
|
cancellationToken);
|
||||||
|
return access.ContainsKey(complaintId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record ComplaintAccessInfo(
|
||||||
|
int ComplaintId,
|
||||||
|
string OwnerUsername,
|
||||||
|
bool OwnedByCurrentUser,
|
||||||
|
bool AccessibleByGroup,
|
||||||
|
IReadOnlyList<string> OwnerGroupCodes)
|
||||||
|
{
|
||||||
|
public bool RequiresOwnerConfirmation =>
|
||||||
|
!OwnedByCurrentUser &&
|
||||||
|
AccessibleByGroup &&
|
||||||
|
!string.IsNullOrWhiteSpace(OwnerUsername);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,378 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using GestionaDenuncias.Shared.Models;
|
||||||
|
using MySqlConnector;
|
||||||
|
|
||||||
|
namespace ApiDenuncias.Services;
|
||||||
|
|
||||||
|
public sealed class WorkGroupAdministrationService
|
||||||
|
{
|
||||||
|
private readonly IDenunciaStore _denunciaStore;
|
||||||
|
private readonly MySqlConnectionStringProvider _connectionStringProvider;
|
||||||
|
|
||||||
|
public WorkGroupAdministrationService(
|
||||||
|
IDenunciaStore denunciaStore,
|
||||||
|
MySqlConnectionStringProvider connectionStringProvider)
|
||||||
|
{
|
||||||
|
_denunciaStore = denunciaStore;
|
||||||
|
_connectionStringProvider = connectionStringProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorkGroupAdministrationDto> GetAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||||
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||||
|
|
||||||
|
var groups = await LoadGroupsAsync(connection, cancellationToken);
|
||||||
|
var users = new Dictionary<long, UserGroupBuilder>();
|
||||||
|
|
||||||
|
const string sql = """
|
||||||
|
SELECT
|
||||||
|
au.id,
|
||||||
|
au.username,
|
||||||
|
wg.code AS group_code
|
||||||
|
FROM app_users au
|
||||||
|
LEFT JOIN app_user_groups aug ON aug.app_user_id = au.id
|
||||||
|
LEFT JOIN work_groups wg
|
||||||
|
ON wg.id = aug.work_group_id
|
||||||
|
AND wg.is_active = 1
|
||||||
|
ORDER BY au.username, wg.code;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var command = new MySqlCommand(sql, connection);
|
||||||
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||||
|
while (await reader.ReadAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
var userId = reader.GetInt64(reader.GetOrdinal("id"));
|
||||||
|
if (!users.TryGetValue(userId, out var user))
|
||||||
|
{
|
||||||
|
user = new UserGroupBuilder(
|
||||||
|
userId,
|
||||||
|
reader.GetString(reader.GetOrdinal("username")));
|
||||||
|
users[userId] = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupOrdinal = reader.GetOrdinal("group_code");
|
||||||
|
if (!reader.IsDBNull(groupOrdinal))
|
||||||
|
{
|
||||||
|
user.GroupCodes.Add(reader.GetString(groupOrdinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WorkGroupAdministrationDto(
|
||||||
|
groups,
|
||||||
|
users.Values
|
||||||
|
.Select(user => new UserWorkGroupDto(
|
||||||
|
user.UserId,
|
||||||
|
user.Username,
|
||||||
|
user.GroupCodes.OrderBy(code => code, StringComparer.Ordinal).ToArray()))
|
||||||
|
.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorkGroupAdministrationDto> UpdateUserGroupsAsync(
|
||||||
|
string username,
|
||||||
|
IEnumerable<string> groupCodes,
|
||||||
|
string changedByUsername,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _denunciaStore.EnsureSchemaAsync(cancellationToken);
|
||||||
|
|
||||||
|
var normalizedUsername = username?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedUsername))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Debes indicar el usuario que se va a configurar.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalizedCodes = groupCodes
|
||||||
|
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||||
|
.Select(code => code.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (normalizedCodes.Length == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cada usuario debe pertenecer al menos a un grupo de trabajo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var connection = await OpenConnectionAsync(cancellationToken);
|
||||||
|
await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var validGroups = await LoadGroupIdsAsync(
|
||||||
|
connection,
|
||||||
|
(MySqlTransaction)transaction,
|
||||||
|
normalizedCodes,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (validGroups.Count != normalizedCodes.Length)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Se ha indicado un grupo de trabajo que no existe o no esta activo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = await EnsureUserAsync(
|
||||||
|
connection,
|
||||||
|
(MySqlTransaction)transaction,
|
||||||
|
normalizedUsername,
|
||||||
|
cancellationToken);
|
||||||
|
var existingGroupIds = await LoadUserGroupIdsAsync(
|
||||||
|
connection,
|
||||||
|
(MySqlTransaction)transaction,
|
||||||
|
userId,
|
||||||
|
cancellationToken);
|
||||||
|
var desiredGroupIds = validGroups.Values.ToHashSet();
|
||||||
|
|
||||||
|
foreach (var removedGroupId in existingGroupIds.Except(desiredGroupIds))
|
||||||
|
{
|
||||||
|
await DeleteMembershipAsync(
|
||||||
|
connection,
|
||||||
|
(MySqlTransaction)transaction,
|
||||||
|
userId,
|
||||||
|
removedGroupId,
|
||||||
|
cancellationToken);
|
||||||
|
await AddHistoryAsync(
|
||||||
|
connection,
|
||||||
|
(MySqlTransaction)transaction,
|
||||||
|
userId,
|
||||||
|
removedGroupId,
|
||||||
|
"removed",
|
||||||
|
changedByUsername,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var addedGroupId in desiredGroupIds.Except(existingGroupIds))
|
||||||
|
{
|
||||||
|
await AddMembershipAsync(
|
||||||
|
connection,
|
||||||
|
(MySqlTransaction)transaction,
|
||||||
|
userId,
|
||||||
|
addedGroupId,
|
||||||
|
changedByUsername,
|
||||||
|
cancellationToken);
|
||||||
|
await AddHistoryAsync(
|
||||||
|
connection,
|
||||||
|
(MySqlTransaction)transaction,
|
||||||
|
userId,
|
||||||
|
addedGroupId,
|
||||||
|
"added",
|
||||||
|
changedByUsername,
|
||||||
|
cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await GetAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<List<WorkGroupDto>> LoadGroupsAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT code, name
|
||||||
|
FROM work_groups
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY code;
|
||||||
|
""";
|
||||||
|
|
||||||
|
var groups = new List<WorkGroupDto>();
|
||||||
|
await using var command = new MySqlCommand(sql, connection);
|
||||||
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||||
|
while (await reader.ReadAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
groups.Add(new WorkGroupDto(
|
||||||
|
reader.GetString(reader.GetOrdinal("code")),
|
||||||
|
reader.GetString(reader.GetOrdinal("name"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Dictionary<string, long>> LoadGroupIdsAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
MySqlTransaction transaction,
|
||||||
|
IReadOnlyList<string> codes,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
await using var command = new MySqlCommand { Connection = connection, Transaction = transaction };
|
||||||
|
var parameters = new List<string>(codes.Count);
|
||||||
|
for (var index = 0; index < codes.Count; index++)
|
||||||
|
{
|
||||||
|
var parameterName = $"@code{index}";
|
||||||
|
parameters.Add(parameterName);
|
||||||
|
command.Parameters.AddWithValue(parameterName, codes[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
command.CommandText = $"""
|
||||||
|
SELECT id, code
|
||||||
|
FROM work_groups
|
||||||
|
WHERE is_active = 1
|
||||||
|
AND code IN ({string.Join(", ", parameters)});
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||||
|
while (await reader.ReadAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
result[reader.GetString(reader.GetOrdinal("code"))] =
|
||||||
|
reader.GetInt64(reader.GetOrdinal("id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<long> EnsureUserAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
MySqlTransaction transaction,
|
||||||
|
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, transaction))
|
||||||
|
{
|
||||||
|
insert.Parameters.AddWithValue("@username", username);
|
||||||
|
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, transaction);
|
||||||
|
select.Parameters.AddWithValue("@username", username);
|
||||||
|
var result = await select.ExecuteScalarAsync(cancellationToken);
|
||||||
|
return Convert.ToInt64(result, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<HashSet<long>> LoadUserGroupIdsAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
MySqlTransaction transaction,
|
||||||
|
long userId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
SELECT work_group_id
|
||||||
|
FROM app_user_groups
|
||||||
|
WHERE app_user_id = @userId;
|
||||||
|
""";
|
||||||
|
|
||||||
|
var result = new HashSet<long>();
|
||||||
|
await using var command = new MySqlCommand(sql, connection, transaction);
|
||||||
|
command.Parameters.AddWithValue("@userId", userId);
|
||||||
|
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||||
|
while (await reader.ReadAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
result.Add(reader.GetInt64(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task DeleteMembershipAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
MySqlTransaction transaction,
|
||||||
|
long userId,
|
||||||
|
long groupId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
DELETE FROM app_user_groups
|
||||||
|
WHERE app_user_id = @userId
|
||||||
|
AND work_group_id = @groupId;
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var command = new MySqlCommand(sql, connection, transaction);
|
||||||
|
command.Parameters.AddWithValue("@userId", userId);
|
||||||
|
command.Parameters.AddWithValue("@groupId", groupId);
|
||||||
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task AddMembershipAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
MySqlTransaction transaction,
|
||||||
|
long userId,
|
||||||
|
long groupId,
|
||||||
|
string changedByUsername,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
INSERT INTO app_user_groups (
|
||||||
|
app_user_id,
|
||||||
|
work_group_id,
|
||||||
|
assigned_by_username
|
||||||
|
) VALUES (
|
||||||
|
@userId,
|
||||||
|
@groupId,
|
||||||
|
@changedByUsername
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
assigned_by_username = VALUES(assigned_by_username);
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var command = new MySqlCommand(sql, connection, transaction);
|
||||||
|
command.Parameters.AddWithValue("@userId", userId);
|
||||||
|
command.Parameters.AddWithValue("@groupId", groupId);
|
||||||
|
command.Parameters.AddWithValue("@changedByUsername", changedByUsername?.Trim() ?? string.Empty);
|
||||||
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task AddHistoryAsync(
|
||||||
|
MySqlConnection connection,
|
||||||
|
MySqlTransaction transaction,
|
||||||
|
long userId,
|
||||||
|
long groupId,
|
||||||
|
string action,
|
||||||
|
string changedByUsername,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
const string sql = """
|
||||||
|
INSERT INTO app_user_group_history (
|
||||||
|
app_user_id,
|
||||||
|
work_group_id,
|
||||||
|
action,
|
||||||
|
changed_by_username
|
||||||
|
) VALUES (
|
||||||
|
@userId,
|
||||||
|
@groupId,
|
||||||
|
@action,
|
||||||
|
@changedByUsername
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
|
||||||
|
await using var command = new MySqlCommand(sql, connection, transaction);
|
||||||
|
command.Parameters.AddWithValue("@userId", userId);
|
||||||
|
command.Parameters.AddWithValue("@groupId", groupId);
|
||||||
|
command.Parameters.AddWithValue("@action", action);
|
||||||
|
command.Parameters.AddWithValue("@changedByUsername", changedByUsername?.Trim() ?? string.Empty);
|
||||||
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var connectionString = await _connectionStringProvider.GetConnectionStringAsync(cancellationToken);
|
||||||
|
var connection = new MySqlConnection(connectionString);
|
||||||
|
await connection.OpenAsync(cancellationToken);
|
||||||
|
await using var command = new MySqlCommand("SET time_zone = '+00:00';", connection);
|
||||||
|
await command.ExecuteNonQueryAsync(cancellationToken);
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record UserGroupBuilder(long UserId, string Username)
|
||||||
|
{
|
||||||
|
public HashSet<string> GroupCodes { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,9 @@ public sealed record InboxSnapshotResponse(
|
|||||||
IReadOnlyList<ReportDto> Reports,
|
IReadOnlyList<ReportDto> Reports,
|
||||||
InboxUserState UserState);
|
InboxUserState UserState);
|
||||||
|
|
||||||
public sealed record ImportReportRequest(ReportDto Report);
|
public sealed record ImportReportRequest(
|
||||||
|
ReportDto Report,
|
||||||
|
bool ConfirmDifferentOwner = false);
|
||||||
|
|
||||||
public sealed record MarkFicherosUploadedRequest(
|
public sealed record MarkFicherosUploadedRequest(
|
||||||
IReadOnlyList<string> FileNames,
|
IReadOnlyList<string> FileNames,
|
||||||
@@ -43,7 +45,8 @@ public sealed record MarkReportHandledInGestionaRequest(
|
|||||||
|
|
||||||
public sealed record TrackingImportPermissionRequest(
|
public sealed record TrackingImportPermissionRequest(
|
||||||
string Username,
|
string Username,
|
||||||
ReportDto Report);
|
ReportDto Report,
|
||||||
|
bool ConfirmDifferentOwner = false);
|
||||||
|
|
||||||
public sealed record GestionaCreateFileRequest(
|
public sealed record GestionaCreateFileRequest(
|
||||||
string Subject,
|
string Subject,
|
||||||
@@ -109,6 +112,22 @@ public sealed record AppConfigurationDto(
|
|||||||
|
|
||||||
public sealed record UpdateExternalUpdateCutoffRequest(string? Date);
|
public sealed record UpdateExternalUpdateCutoffRequest(string? Date);
|
||||||
|
|
||||||
|
public sealed record WorkGroupDto(
|
||||||
|
string Code,
|
||||||
|
string Name);
|
||||||
|
|
||||||
|
public sealed record UserWorkGroupDto(
|
||||||
|
long UserId,
|
||||||
|
string Username,
|
||||||
|
IReadOnlyList<string> GroupCodes);
|
||||||
|
|
||||||
|
public sealed record WorkGroupAdministrationDto(
|
||||||
|
IReadOnlyList<WorkGroupDto> Groups,
|
||||||
|
IReadOnlyList<UserWorkGroupDto> Users);
|
||||||
|
|
||||||
|
public sealed record UpdateUserWorkGroupsRequest(
|
||||||
|
IReadOnlyList<string> GroupCodes);
|
||||||
|
|
||||||
public sealed record GestionaComplaintFieldsResponse(
|
public sealed record GestionaComplaintFieldsResponse(
|
||||||
DateTime? FechaDenuncia,
|
DateTime? FechaDenuncia,
|
||||||
int NumeroDenunciaCanal,
|
int NumeroDenunciaCanal,
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ public class DenunciasGestiona
|
|||||||
public bool EnGestiona { get; set; }
|
public bool EnGestiona { get; set; }
|
||||||
public bool EnRechazada { get; set; }
|
public bool EnRechazada { get; set; }
|
||||||
|
|
||||||
|
public string OwnerUsername { get; set; } = string.Empty;
|
||||||
|
public bool OwnedByCurrentUser { get; set; }
|
||||||
|
public bool RequiresOwnerConfirmation { get; set; }
|
||||||
|
public IReadOnlyList<string> OwnerWorkGroups { get; set; } = [];
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public DateOnly? KeyDate { get; set; }
|
public DateOnly? KeyDate { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,17 @@ public sealed record ReportDetailDto(
|
|||||||
string? LastAccess,
|
string? LastAccess,
|
||||||
IReadOnlyList<ReportCommentDto> Comments,
|
IReadOnlyList<ReportCommentDto> Comments,
|
||||||
IReadOnlyList<ReportFileDto> WhistleblowerFiles,
|
IReadOnlyList<ReportFileDto> WhistleblowerFiles,
|
||||||
IReadOnlyList<ReportFileDto> ReceiverFiles);
|
IReadOnlyList<ReportFileDto> ReceiverFiles,
|
||||||
|
IReadOnlyList<ReportReceiverDto>? Receivers = null);
|
||||||
|
|
||||||
public sealed record ReportCommentDto(
|
public sealed record ReportCommentDto(
|
||||||
string? Id,
|
string? Id,
|
||||||
string? Type,
|
string? Type,
|
||||||
string? Content,
|
string? Content,
|
||||||
string? CreationDate,
|
string? CreationDate,
|
||||||
bool IsNew);
|
bool IsNew,
|
||||||
|
string? AuthorId = null,
|
||||||
|
string? AuthorName = null);
|
||||||
|
|
||||||
public sealed record ReportFileDto(
|
public sealed record ReportFileDto(
|
||||||
string? Id,
|
string? Id,
|
||||||
@@ -20,4 +23,11 @@ public sealed record ReportFileDto(
|
|||||||
long? Size,
|
long? Size,
|
||||||
string? ContentType,
|
string? ContentType,
|
||||||
string? CreationDate,
|
string? CreationDate,
|
||||||
bool IsNew);
|
bool IsNew,
|
||||||
|
string? AuthorId = null,
|
||||||
|
string? AuthorName = null);
|
||||||
|
|
||||||
|
public sealed record ReportReceiverDto(
|
||||||
|
string Id,
|
||||||
|
string Name,
|
||||||
|
bool Active);
|
||||||
|
|||||||
@@ -24,11 +24,18 @@ public sealed record ReportDto
|
|||||||
public bool CitizenHasNewFile { get; init; }
|
public bool CitizenHasNewFile { get; init; }
|
||||||
public string? ReceiverLastActivity { get; init; }
|
public string? ReceiverLastActivity { get; init; }
|
||||||
public bool ReceiverHasNewActivity { get; init; }
|
public bool ReceiverHasNewActivity { get; init; }
|
||||||
|
public string? ReceiverLastActivityAuthorId { get; init; }
|
||||||
|
public string? ReceiverLastActivityAuthorName { get; init; }
|
||||||
public bool DownloadedByCurrentUser { get; init; }
|
public bool DownloadedByCurrentUser { get; init; }
|
||||||
public bool DownloadedByAnotherUser { get; init; }
|
public bool DownloadedByAnotherUser { get; init; }
|
||||||
public string? LastDownloadedByUsername { get; init; }
|
public string? LastDownloadedByUsername { get; init; }
|
||||||
public string? LastDownloadedAt { get; init; }
|
public string? LastDownloadedAt { get; init; }
|
||||||
public bool AlreadyImported { get; init; }
|
public bool AlreadyImported { get; init; }
|
||||||
public bool AlreadyInGestiona { get; init; }
|
public bool AlreadyInGestiona { get; init; }
|
||||||
|
public string? OwnerUsername { get; init; }
|
||||||
|
public bool OwnedByCurrentUser { get; init; }
|
||||||
|
public bool AccessibleByWorkGroup { get; init; }
|
||||||
|
public bool RequiresOwnerConfirmation { get; init; }
|
||||||
|
public IReadOnlyList<string> OwnerWorkGroups { get; init; } = [];
|
||||||
public string? TrackingNote { get; init; }
|
public string? TrackingNote { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,6 @@ public interface IInboxTrackingService
|
|||||||
Task EnsureReportCanBeImportedByUserAsync(
|
Task EnsureReportCanBeImportedByUserAsync(
|
||||||
string username,
|
string username,
|
||||||
ReportDto report,
|
ReportDto report,
|
||||||
|
bool confirmDifferentOwner = false,
|
||||||
CancellationToken cancellationToken = default);
|
CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
@inject IDenunciaStore DenunciaStore
|
@inject IDenunciaStore DenunciaStore
|
||||||
@inject ApiDenunciasClient ApiDenuncias
|
@inject ApiDenunciasClient ApiDenuncias
|
||||||
@inject UiBusyService Busy
|
@inject UiBusyService Busy
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
<PageTitle>Actualizaciones</PageTitle>
|
<PageTitle>Actualizaciones</PageTitle>
|
||||||
|
|
||||||
@@ -162,7 +163,21 @@ else
|
|||||||
data-bs-target="#@collapseId"
|
data-bs-target="#@collapseId"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="@collapseId">
|
aria-controls="@collapseId">
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia (Actualización)</h5>
|
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia (Actualización)</h5>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(denuncia.OwnerUsername))
|
||||||
|
{
|
||||||
|
<span class="badge @(denuncia.OwnedByCurrentUser ? "text-bg-secondary" : "text-bg-warning")">
|
||||||
|
Responsable: @denuncia.OwnerUsername
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
@if (denuncia.OwnerWorkGroups.Count > 0)
|
||||||
|
{
|
||||||
|
<span class="badge text-bg-light">
|
||||||
|
Grupo: @string.Join(" / ", denuncia.OwnerWorkGroups)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="text-muted small me-3">
|
<div class="text-muted small me-3">
|
||||||
@@ -703,6 +718,18 @@ else
|
|||||||
|
|
||||||
private async Task OpenEnviarAGestionaModal(DenunciasGestiona d)
|
private async Task OpenEnviarAGestionaModal(DenunciasGestiona d)
|
||||||
{
|
{
|
||||||
|
if (d.RequiresOwnerConfirmation)
|
||||||
|
{
|
||||||
|
var confirmed = await JSRuntime.InvokeAsync<bool>(
|
||||||
|
"confirm",
|
||||||
|
$"La denuncia #{d.Id_Denuncia} es propiedad de {d.OwnerUsername}. " +
|
||||||
|
"Puedes actualizarla porque pertenece a un usuario de tu grupo. Deseas continuar?");
|
||||||
|
if (!confirmed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
selectedDenuncias = d;
|
selectedDenuncias = d;
|
||||||
nuevoAsunto = string.IsNullOrWhiteSpace(d.NombreDenuncia) ? $"Denuncia-{d.Id_Denuncia}-CD" : d.NombreDenuncia;
|
nuevoAsunto = string.IsNullOrWhiteSpace(d.NombreDenuncia) ? $"Denuncia-{d.Id_Denuncia}-CD" : d.NombreDenuncia;
|
||||||
nombreDocumentos = "";
|
nombreDocumentos = "";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@using GestionaDenunciasAN.Services
|
@using GestionaDenunciasAN.Services
|
||||||
|
@using GestionaDenuncias.Shared.Models
|
||||||
@using Microsoft.AspNetCore.Components.Authorization
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
|
||||||
@inject ApiDenunciasClient ApiDenuncias
|
@inject ApiDenunciasClient ApiDenuncias
|
||||||
@@ -95,6 +96,104 @@ else
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
||||||
|
<div>
|
||||||
|
<h5 class="mb-1">Grupos de trabajo</h5>
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
Los usuarios pueden consultar y tratar denuncias de otros propietarios cuando comparten al menos un grupo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-secondary btn-sm"
|
||||||
|
disabled="@isLoadingWorkGroups"
|
||||||
|
@onclick="LoadWorkGroupsAsync">
|
||||||
|
Actualizar usuarios
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (isLoadingWorkGroups)
|
||||||
|
{
|
||||||
|
<div class="text-muted">
|
||||||
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||||
|
Cargando usuarios y grupos...
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (workGroupAdministration is null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning mb-0">No se ha podido cargar la configuracion de grupos.</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Usuario</th>
|
||||||
|
@foreach (var group in workGroupAdministration.Groups)
|
||||||
|
{
|
||||||
|
<th>
|
||||||
|
<span class="d-block">@group.Code</span>
|
||||||
|
<small class="text-muted fw-normal">@group.Name</small>
|
||||||
|
</th>
|
||||||
|
}
|
||||||
|
<th class="text-end">Accion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var user in workGroupAdministration.Users)
|
||||||
|
{
|
||||||
|
<tr @key="user.UserId">
|
||||||
|
<td>
|
||||||
|
<strong>@user.Username</strong>
|
||||||
|
@if (!GetSelectedGroups(user.UserId).Any())
|
||||||
|
{
|
||||||
|
<span class="badge text-bg-warning ms-2">Sin grupo</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
@foreach (var group in workGroupAdministration.Groups)
|
||||||
|
{
|
||||||
|
var checkboxId = $"user-group-{user.UserId}-{group.Code}";
|
||||||
|
<td>
|
||||||
|
<div class="form-check">
|
||||||
|
<input id="@checkboxId"
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked="@GetSelectedGroups(user.UserId).Contains(group.Code)"
|
||||||
|
disabled="@savingWorkGroupUsers.Contains(user.UserId)"
|
||||||
|
@onchange="args => ToggleUserGroup(user.UserId, group.Code, args)" />
|
||||||
|
<label class="form-check-label" for="@checkboxId">@group.Code</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
|
<td class="text-end">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
disabled="@savingWorkGroupUsers.Contains(user.UserId)"
|
||||||
|
@onclick="() => SaveUserGroupsAsync(user)">
|
||||||
|
@(savingWorkGroupUsers.Contains(user.UserId) ? "Guardando..." : "Guardar")
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(workGroupNotice))
|
||||||
|
{
|
||||||
|
<div class="alert alert-success mt-3 mb-0">@workGroupNotice</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(workGroupError))
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger mt-3 mb-0">@workGroupError</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card mt-3">
|
<div class="card mt-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="mb-3">Purga manual con reemplazo</h5>
|
<h5 class="mb-3">Purga manual con reemplazo</h5>
|
||||||
@@ -223,6 +322,12 @@ else
|
|||||||
private string? configurationNotice;
|
private string? configurationNotice;
|
||||||
private string? configurationError;
|
private string? configurationError;
|
||||||
private bool isSavingConfiguration;
|
private bool isSavingConfiguration;
|
||||||
|
private WorkGroupAdministrationDto? workGroupAdministration;
|
||||||
|
private readonly Dictionary<long, HashSet<string>> selectedWorkGroups = [];
|
||||||
|
private readonly HashSet<long> savingWorkGroupUsers = [];
|
||||||
|
private bool isLoadingWorkGroups;
|
||||||
|
private string? workGroupNotice;
|
||||||
|
private string? workGroupError;
|
||||||
|
|
||||||
private string confirmation = string.Empty;
|
private string confirmation = string.Empty;
|
||||||
private bool acceptedRisk;
|
private bool acceptedRisk;
|
||||||
@@ -246,6 +351,7 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
await LoadConfigurationAsync();
|
await LoadConfigurationAsync();
|
||||||
|
await LoadWorkGroupsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadConfigurationAsync()
|
private async Task LoadConfigurationAsync()
|
||||||
@@ -297,6 +403,98 @@ else
|
|||||||
await SaveExternalUpdateCutoffDateAsync();
|
await SaveExternalUpdateCutoffDateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LoadWorkGroupsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isLoadingWorkGroups = true;
|
||||||
|
workGroupError = null;
|
||||||
|
var response = await ApiDenuncias.GetWorkGroupAdministrationAsync();
|
||||||
|
ApplyWorkGroupAdministration(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
workGroupError = $"No se han podido cargar los grupos de trabajo: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isLoadingWorkGroups = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyWorkGroupAdministration(WorkGroupAdministrationDto response)
|
||||||
|
{
|
||||||
|
workGroupAdministration = response;
|
||||||
|
selectedWorkGroups.Clear();
|
||||||
|
foreach (var user in response.Users)
|
||||||
|
{
|
||||||
|
selectedWorkGroups[user.UserId] = user.GroupCodes.ToHashSet(
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashSet<string> GetSelectedGroups(long userId)
|
||||||
|
{
|
||||||
|
if (!selectedWorkGroups.TryGetValue(userId, out var groups))
|
||||||
|
{
|
||||||
|
groups = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
selectedWorkGroups[userId] = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleUserGroup(
|
||||||
|
long userId,
|
||||||
|
string groupCode,
|
||||||
|
ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
var groups = GetSelectedGroups(userId);
|
||||||
|
var enabled = args.Value is bool boolValue
|
||||||
|
? boolValue
|
||||||
|
: bool.TryParse(args.Value?.ToString(), out var parsed) && parsed;
|
||||||
|
|
||||||
|
if (enabled)
|
||||||
|
{
|
||||||
|
groups.Add(groupCode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
groups.Remove(groupCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SaveUserGroupsAsync(UserWorkGroupDto user)
|
||||||
|
{
|
||||||
|
var groups = GetSelectedGroups(user.UserId);
|
||||||
|
if (groups.Count == 0)
|
||||||
|
{
|
||||||
|
workGroupNotice = null;
|
||||||
|
workGroupError = $"El usuario {user.Username} debe pertenecer al menos a un grupo.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
savingWorkGroupUsers.Add(user.UserId);
|
||||||
|
workGroupNotice = null;
|
||||||
|
workGroupError = null;
|
||||||
|
var response = await ApiDenuncias.UpdateUserWorkGroupsAsync(
|
||||||
|
user.Username,
|
||||||
|
groups.OrderBy(code => code, StringComparer.Ordinal).ToArray());
|
||||||
|
ApplyWorkGroupAdministration(response);
|
||||||
|
workGroupNotice = $"Grupos de {user.Username} actualizados correctamente.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
workGroupError = $"No se han podido guardar los grupos de {user.Username}: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
savingWorkGroupUsers.Remove(user.UserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string? ToIsoDateText(string? value)
|
private static string? ToIsoDateText(string? value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
|||||||
@@ -122,6 +122,9 @@
|
|||||||
|
|
||||||
.inbox-activity-cell {
|
.inbox-activity-cell {
|
||||||
width: 9.25rem;
|
width: 9.25rem;
|
||||||
|
max-width: 12rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inbox-action-cell {
|
.inbox-action-cell {
|
||||||
@@ -226,6 +229,7 @@
|
|||||||
<option value="all">Todas</option>
|
<option value="all">Todas</option>
|
||||||
<option value="new">Nuevas / sin leer</option>
|
<option value="new">Nuevas / sin leer</option>
|
||||||
<option value="updated">Actualizaciones del ciudadano</option>
|
<option value="updated">Actualizaciones del ciudadano</option>
|
||||||
|
<option value="receiver">Actividad OAAF</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -355,7 +359,7 @@
|
|||||||
<td class="inbox-channel-cell" title="@(report.ContextName ?? report.ContextId ?? string.Empty)"><span class="d-inline-block text-truncate">@(report.ContextName ?? report.ContextId ?? "-")</span></td>
|
<td class="inbox-channel-cell" title="@(report.ContextName ?? report.ContextId ?? string.Empty)"><span class="d-inline-block text-truncate">@(report.ContextName ?? report.ContextId ?? "-")</span></td>
|
||||||
<td>@FormatDate(report.CreationDate)</td>
|
<td>@FormatDate(report.CreationDate)</td>
|
||||||
<td class="inbox-activity-cell">@FormatCitizenActivity(report)</td>
|
<td class="inbox-activity-cell">@FormatCitizenActivity(report)</td>
|
||||||
<td class="inbox-activity-cell">@FormatReceiverActivity(report)</td>
|
<td class="inbox-activity-cell" title="@GetReceiverActivityTitle(report)">@FormatReceiverActivity(report)</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge @GetStatusBadgeCss(report)">
|
<span class="badge @GetStatusBadgeCss(report)">
|
||||||
@GetStatusLabel(report)
|
@GetStatusLabel(report)
|
||||||
@@ -432,7 +436,7 @@
|
|||||||
{
|
{
|
||||||
<div class="border rounded p-3 mb-2 @(comment.IsNew ? "border-success bg-success-subtle" : "bg-light")">
|
<div class="border rounded p-3 mb-2 @(comment.IsNew ? "border-success bg-success-subtle" : "bg-light")">
|
||||||
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 small text-muted mb-2">
|
<div class="d-flex flex-column flex-md-row justify-content-between gap-2 small text-muted mb-2">
|
||||||
<strong>@GetCommentAuthorLabel(comment.Type)</strong>
|
<strong>@GetCommentAuthorLabel(comment)</strong>
|
||||||
<span>@FormatDate(comment.CreationDate)</span>
|
<span>@FormatDate(comment.CreationDate)</span>
|
||||||
</div>
|
</div>
|
||||||
@if (comment.IsNew)
|
@if (comment.IsNew)
|
||||||
@@ -482,6 +486,10 @@
|
|||||||
<span class="small text-muted">@FormatDate(file.CreationDate)</span>
|
<span class="small text-muted">@FormatDate(file.CreationDate)</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="small text-muted">@FormatBytes(file.Size) @(string.IsNullOrWhiteSpace(file.ContentType) ? string.Empty : $" - {file.ContentType}")</div>
|
<div class="small text-muted">@FormatBytes(file.Size) @(string.IsNullOrWhiteSpace(file.ContentType) ? string.Empty : $" - {file.ContentType}")</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(file.AuthorName))
|
||||||
|
{
|
||||||
|
<div class="small text-muted">Añadido por @file.AuthorName</div>
|
||||||
|
}
|
||||||
@if (file.IsNew)
|
@if (file.IsNew)
|
||||||
{
|
{
|
||||||
<span class="badge bg-success mt-2">Nuevo</span>
|
<span class="badge bg-success mt-2">Nuevo</span>
|
||||||
@@ -703,13 +711,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportBusy = true;
|
|
||||||
var importedCount = 0;
|
|
||||||
var errors = new List<string>();
|
|
||||||
var importWarnings = new List<string>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var selectedReports = Reports
|
var selectedReports = Reports
|
||||||
.Where(report => SelectedIds.Contains(report.Id))
|
.Where(report => SelectedIds.Contains(report.Id))
|
||||||
.Where(CanUseReport)
|
.Where(CanUseReport)
|
||||||
@@ -722,6 +723,31 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reportsFromOtherOwners = selectedReports
|
||||||
|
.Where(report => report.RequiresOwnerConfirmation)
|
||||||
|
.ToArray();
|
||||||
|
if (reportsFromOtherOwners.Length > 0)
|
||||||
|
{
|
||||||
|
var ownerSummary = string.Join(
|
||||||
|
Environment.NewLine,
|
||||||
|
reportsFromOtherOwners.Select(report =>
|
||||||
|
$"Denuncia #{report.Progressive ?? 0}: propiedad de {report.OwnerUsername}."));
|
||||||
|
var confirmed = await JSRuntime.InvokeAsync<bool>(
|
||||||
|
"confirm",
|
||||||
|
$"Vas a importar denuncias propiedad de otros usuarios de tu grupo:{Environment.NewLine}{Environment.NewLine}{ownerSummary}{Environment.NewLine}{Environment.NewLine}¿Deseas continuar?");
|
||||||
|
if (!confirmed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportBusy = true;
|
||||||
|
var importedCount = 0;
|
||||||
|
var errors = new List<string>();
|
||||||
|
var importWarnings = new List<string>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
using var busy = Busy.Show(
|
using var busy = Busy.Show(
|
||||||
"Importando denuncias",
|
"Importando denuncias",
|
||||||
$"Descargando y procesando {selectedReports.Count} denuncia(s) desde GlobalLeaks.",
|
$"Descargando y procesando {selectedReports.Count} denuncia(s) desde GlobalLeaks.",
|
||||||
@@ -739,7 +765,10 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await ApiDenuncias.ImportReportAsync(report, CancellationToken.None);
|
var result = await ApiDenuncias.ImportReportAsync(
|
||||||
|
report,
|
||||||
|
report.RequiresOwnerConfirmation,
|
||||||
|
CancellationToken.None);
|
||||||
importedCount += result.ImportedCount;
|
importedCount += result.ImportedCount;
|
||||||
errors.AddRange(result.Errors.Select(error => $"#{report.Progressive ?? 0}: {error}"));
|
errors.AddRange(result.Errors.Select(error => $"#{report.Progressive ?? 0}: {error}"));
|
||||||
if (result.Warnings is not null)
|
if (result.Warnings is not null)
|
||||||
@@ -898,7 +927,10 @@
|
|||||||
filtered = Filter switch
|
filtered = Filter switch
|
||||||
{
|
{
|
||||||
"new" => filtered.Where(report => string.IsNullOrWhiteSpace(report.AccessDate) || string.Equals(report.Status, "new", StringComparison.OrdinalIgnoreCase)),
|
"new" => filtered.Where(report => string.IsNullOrWhiteSpace(report.AccessDate) || string.Equals(report.Status, "new", StringComparison.OrdinalIgnoreCase)),
|
||||||
"updated" => filtered.Where(report => report.CitizenHasNewActivity || report.Updated),
|
"updated" => filtered.Where(report =>
|
||||||
|
report.CitizenHasNewActivity ||
|
||||||
|
(!report.ActivityAnalyzed && report.Updated)),
|
||||||
|
"receiver" => filtered.Where(report => report.ReceiverHasNewActivity),
|
||||||
_ => filtered
|
_ => filtered
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1144,7 +1176,9 @@
|
|||||||
var receiverDate = FormatOptionalDate(report.ReceiverLastActivity);
|
var receiverDate = FormatOptionalDate(report.ReceiverLastActivity);
|
||||||
if (!string.IsNullOrWhiteSpace(receiverDate))
|
if (!string.IsNullOrWhiteSpace(receiverDate))
|
||||||
{
|
{
|
||||||
return receiverDate;
|
return string.IsNullOrWhiteSpace(report.ReceiverLastActivityAuthorName)
|
||||||
|
? receiverDate
|
||||||
|
: $"{receiverDate} · {report.ReceiverLastActivityAuthorName}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!report.ActivityAnalyzed)
|
if (!report.ActivityAnalyzed)
|
||||||
@@ -1155,6 +1189,16 @@
|
|||||||
return "Sin actividad";
|
return "Sin actividad";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetReceiverActivityTitle(ReportDto report)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(report.ReceiverLastActivityAuthorName))
|
||||||
|
{
|
||||||
|
return FormatReceiverActivity(report);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"Ultima actividad OAAF realizada por {report.ReceiverLastActivityAuthorName}.";
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatBytes(long? value)
|
private static string FormatBytes(long? value)
|
||||||
{
|
{
|
||||||
if (value is null or <= 0)
|
if (value is null or <= 0)
|
||||||
@@ -1176,11 +1220,13 @@
|
|||||||
return $"{bytes / 1024d / 1024d:0.#} MB";
|
return $"{bytes / 1024d / 1024d:0.#} MB";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetCommentAuthorLabel(string? type)
|
private static string GetCommentAuthorLabel(ReportCommentDto comment)
|
||||||
=> IsWhistleblowerActivityType(type)
|
=> IsWhistleblowerActivityType(comment.Type)
|
||||||
? "Denunciante"
|
? "Denunciante"
|
||||||
: IsReceiverActivityType(type)
|
: IsReceiverActivityType(comment.Type)
|
||||||
? "Receptor"
|
? string.IsNullOrWhiteSpace(comment.AuthorName)
|
||||||
|
? "Gestor OAAF"
|
||||||
|
: $"Gestor OAAF: {comment.AuthorName}"
|
||||||
: "Comentario";
|
: "Comentario";
|
||||||
|
|
||||||
private static bool IsWhistleblowerActivityType(string? value)
|
private static bool IsWhistleblowerActivityType(string? value)
|
||||||
@@ -1298,7 +1344,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
private static bool CanUseReport(ReportDto report)
|
private static bool CanUseReport(ReportDto report)
|
||||||
=> report.Accessible != false && !IsReceiverOnlyUpdate(report);
|
=> report.Accessible != false;
|
||||||
|
|
||||||
private static bool IsReceiverOnlyUpdate(ReportDto report)
|
private static bool IsReceiverOnlyUpdate(ReportDto report)
|
||||||
=> report.AlreadyInGestiona &&
|
=> report.AlreadyInGestiona &&
|
||||||
@@ -1313,11 +1359,6 @@
|
|||||||
return "GlobalLeaks indica que esta denuncia no es accesible para este usuario.";
|
return "GlobalLeaks indica que esta denuncia no es accesible para este usuario.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsReceiverOnlyUpdate(report))
|
|
||||||
{
|
|
||||||
return "La actividad nueva procede de OAAF/gestor; no se importa como actualizacion del ciudadano.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1328,11 +1369,6 @@
|
|||||||
return "GlobalLeaks indica que esta denuncia no es accesible para este usuario.";
|
return "GlobalLeaks indica que esta denuncia no es accesible para este usuario.";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (IsReceiverOnlyUpdate(report))
|
|
||||||
{
|
|
||||||
return "Actividad interna detectada; puedes consultar el detalle, pero no importarla como actualizacion.";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Consulta mensajes y ficheros. GlobalLeaks puede marcar la denuncia como leida.";
|
return "Consulta mensajes y ficheros. GlobalLeaks puede marcar la denuncia como leida.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1395,7 +1431,7 @@
|
|||||||
|
|
||||||
if (IsReceiverOnlyUpdate(report))
|
if (IsReceiverOnlyUpdate(report))
|
||||||
{
|
{
|
||||||
return "table-secondary report-row-disabled";
|
return "table-secondary";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (report.AlreadyInGestiona)
|
if (report.AlreadyInGestiona)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
@inject IDenunciaStore DenunciaStore
|
@inject IDenunciaStore DenunciaStore
|
||||||
@inject ApiDenunciasClient ApiDenuncias
|
@inject ApiDenunciasClient ApiDenuncias
|
||||||
@inject UiBusyService Busy
|
@inject UiBusyService Busy
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
<PageTitle>Denuncias Pendientes</PageTitle>
|
<PageTitle>Denuncias Pendientes</PageTitle>
|
||||||
|
|
||||||
@@ -199,7 +200,21 @@ else
|
|||||||
data-bs-target="#@collapseId"
|
data-bs-target="#@collapseId"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="@collapseId">
|
aria-controls="@collapseId">
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia</h5>
|
<h5 class="mb-0">Denuncia ID: @denuncia.Id_Denuncia</h5>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(denuncia.OwnerUsername))
|
||||||
|
{
|
||||||
|
<span class="badge @(denuncia.OwnedByCurrentUser ? "text-bg-secondary" : "text-bg-warning")">
|
||||||
|
Responsable: @denuncia.OwnerUsername
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
@if (denuncia.OwnerWorkGroups.Count > 0)
|
||||||
|
{
|
||||||
|
<span class="badge text-bg-light">
|
||||||
|
Grupo: @string.Join(" / ", denuncia.OwnerWorkGroups)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-success btn-sm me-2"
|
class="btn btn-success btn-sm me-2"
|
||||||
@@ -1288,8 +1303,13 @@ else
|
|||||||
await DenunciaStore.UpsertDenunciaAsync(d);
|
await DenunciaStore.UpsertDenunciaAsync(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenEnviarAGestionaModal(DenunciasGestiona d)
|
private async Task OpenEnviarAGestionaModal(DenunciasGestiona d)
|
||||||
{
|
{
|
||||||
|
if (!await ConfirmDifferentOwnerAsync(d, "tramitar"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
selectedDenuncias = d;
|
selectedDenuncias = d;
|
||||||
nuevoAsunto = $"Denuncia {d.Id_Denuncia}-CD";
|
nuevoAsunto = $"Denuncia {d.Id_Denuncia}-CD";
|
||||||
|
|
||||||
@@ -1301,13 +1321,33 @@ else
|
|||||||
showModal = true;
|
showModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenRechazarModal(DenunciasGestiona d)
|
private async Task OpenRechazarModal(DenunciasGestiona d)
|
||||||
{
|
{
|
||||||
|
if (!await ConfirmDifferentOwnerAsync(d, "rechazar"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
selectedDenuncias = d;
|
selectedDenuncias = d;
|
||||||
motivoRechazo = string.Empty;
|
motivoRechazo = string.Empty;
|
||||||
showModalRechazo = true;
|
showModalRechazo = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> ConfirmDifferentOwnerAsync(
|
||||||
|
DenunciasGestiona denuncia,
|
||||||
|
string action)
|
||||||
|
{
|
||||||
|
if (!denuncia.RequiresOwnerConfirmation)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await JSRuntime.InvokeAsync<bool>(
|
||||||
|
"confirm",
|
||||||
|
$"La denuncia #{denuncia.Id_Denuncia} es propiedad de {denuncia.OwnerUsername}. " +
|
||||||
|
$"Puedes {action}la porque pertenece a un usuario de tu grupo. Deseas continuar?");
|
||||||
|
}
|
||||||
|
|
||||||
private void CloseModal()
|
private void CloseModal()
|
||||||
{
|
{
|
||||||
showModal = false;
|
showModal = false;
|
||||||
|
|||||||
@@ -72,11 +72,14 @@ public sealed class ApiDenunciasClient
|
|||||||
public Task<InboxSnapshotResponse> LoadInboxAsync(CancellationToken cancellationToken = default)
|
public Task<InboxSnapshotResponse> LoadInboxAsync(CancellationToken cancellationToken = default)
|
||||||
=> SendAsync<InboxSnapshotResponse>(HttpMethod.Get, "api/inbox/reports", body: null, authorize: true, cancellationToken);
|
=> SendAsync<InboxSnapshotResponse>(HttpMethod.Get, "api/inbox/reports", body: null, authorize: true, cancellationToken);
|
||||||
|
|
||||||
public Task<ImportSummary> ImportReportAsync(ReportDto report, CancellationToken cancellationToken = default)
|
public Task<ImportSummary> ImportReportAsync(
|
||||||
|
ReportDto report,
|
||||||
|
bool confirmDifferentOwner = false,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
=> SendAsync<ImportSummary>(
|
=> SendAsync<ImportSummary>(
|
||||||
HttpMethod.Post,
|
HttpMethod.Post,
|
||||||
$"api/inbox/reports/{Uri.EscapeDataString(report.Id)}/import",
|
$"api/inbox/reports/{Uri.EscapeDataString(report.Id)}/import",
|
||||||
new ImportReportRequest(report),
|
new ImportReportRequest(report, confirmDifferentOwner),
|
||||||
authorize: true,
|
authorize: true,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
@@ -255,6 +258,23 @@ public sealed class ApiDenunciasClient
|
|||||||
authorize: true,
|
authorize: true,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
|
public Task<WorkGroupAdministrationDto> GetWorkGroupAdministrationAsync(
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
=> GetAsync<WorkGroupAdministrationDto>(
|
||||||
|
"api/configuration/work-groups",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
public Task<WorkGroupAdministrationDto> UpdateUserWorkGroupsAsync(
|
||||||
|
string username,
|
||||||
|
IReadOnlyList<string> groupCodes,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
=> SendAsync<WorkGroupAdministrationDto>(
|
||||||
|
HttpMethod.Put,
|
||||||
|
$"api/configuration/work-groups/users/{Uri.EscapeDataString(username)}",
|
||||||
|
new UpdateUserWorkGroupsRequest(groupCodes),
|
||||||
|
authorize: true,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
internal Task<T> GetAsync<T>(string path, CancellationToken cancellationToken = default, bool allowNull = false)
|
internal Task<T> GetAsync<T>(string path, CancellationToken cancellationToken = default, bool allowNull = false)
|
||||||
=> SendAsync<T>(HttpMethod.Get, path, body: null, authorize: true, cancellationToken, allowNull);
|
=> SendAsync<T>(HttpMethod.Get, path, body: null, authorize: true, cancellationToken, allowNull);
|
||||||
|
|
||||||
|
|||||||
@@ -46,9 +46,10 @@ public sealed class ApiInboxTrackingService : IInboxTrackingService
|
|||||||
public Task EnsureReportCanBeImportedByUserAsync(
|
public Task EnsureReportCanBeImportedByUserAsync(
|
||||||
string username,
|
string username,
|
||||||
ReportDto report,
|
ReportDto report,
|
||||||
|
bool confirmDifferentOwner = false,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
=> _api.PostAsync(
|
=> _api.PostAsync(
|
||||||
"api/tracking/import-permission",
|
"api/tracking/import-permission",
|
||||||
new TrackingImportPermissionRequest(username, report),
|
new TrackingImportPermissionRequest(username, report, confirmDifferentOwner),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var separator = certLoginBaseUrl.Contains('?') ? "&" : "?";
|
var separator = certLoginBaseUrl.Contains('?') ? "&" : "?";
|
||||||
var url = $"{certLoginBaseUrl}{separator}iframe=true&parentOrigin={Uri.EscapeDataString(parentOrigin)}";
|
var url =
|
||||||
|
$"{certLoginBaseUrl}{separator}iframe=true&origen=Registro&parentOrigin={Uri.EscapeDataString(parentOrigin)}";
|
||||||
await JSRuntime.InvokeVoidAsync("iniciarSesionConCertificado", url);
|
await JSRuntime.InvokeVoidAsync("iniciarSesionConCertificado", url);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ namespace SwaggerAntifraude.Controllers
|
|||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpGet("login-cert")]
|
[HttpGet("login-cert")]
|
||||||
public IActionResult LoginWithCertificateGet([FromQuery] bool iframe = false)
|
public IActionResult LoginWithCertificateGet([FromQuery] bool iframe = false, [FromQuery] string? origen = null)
|
||||||
{
|
{
|
||||||
var clientCert = HttpContext.Connection.ClientCertificate;
|
var clientCert = HttpContext.Connection.ClientCertificate;
|
||||||
if (clientCert == null)
|
if (clientCert == null)
|
||||||
@@ -56,7 +56,7 @@ namespace SwaggerAntifraude.Controllers
|
|||||||
if (string.IsNullOrWhiteSpace(dni))
|
if (string.IsNullOrWhiteSpace(dni))
|
||||||
return Unauthorized("No se pudo obtener un DNI válido del certificado.");
|
return Unauthorized("No se pudo obtener un DNI válido del certificado.");
|
||||||
|
|
||||||
var result = AuthenticateCertificateDni(dni);
|
var result = AuthenticateCertificateDni(dni, origen);
|
||||||
if (result.Token == null || result.Persona == null)
|
if (result.Token == null || result.Persona == null)
|
||||||
return Unauthorized(result.Error ?? "No se pudo autenticar el certificado.");
|
return Unauthorized(result.Error ?? "No se pudo autenticar el certificado.");
|
||||||
|
|
||||||
@@ -82,14 +82,14 @@ namespace SwaggerAntifraude.Controllers
|
|||||||
return BadRequest("Debe indicarse un DNI.");
|
return BadRequest("Debe indicarse un DNI.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = AuthenticateCertificateDni(request.Dni);
|
var result = AuthenticateCertificateDni(request.Dni, request.Origen);
|
||||||
if (result.Token == null || result.Persona == null)
|
if (result.Token == null || result.Persona == null)
|
||||||
return Unauthorized(result.Error ?? "No se pudo autenticar el certificado.");
|
return Unauthorized(result.Error ?? "No se pudo autenticar el certificado.");
|
||||||
|
|
||||||
return Ok(BuildLoginResponse(result.Persona, result.Token));
|
return Ok(BuildLoginResponse(result.Persona, result.Token));
|
||||||
}
|
}
|
||||||
|
|
||||||
private (string? Token, PERSONAS? Persona, string? Error) AuthenticateCertificateDni(string dni)
|
private (string? Token, PERSONAS? Persona, string? Error) AuthenticateCertificateDni(string dni, string? origen)
|
||||||
{
|
{
|
||||||
using var context = tsGestionAntifraude.NuevoContexto(SoloLectura: true);
|
using var context = tsGestionAntifraude.NuevoContexto(SoloLectura: true);
|
||||||
|
|
||||||
@@ -100,7 +100,8 @@ namespace SwaggerAntifraude.Controllers
|
|||||||
{
|
{
|
||||||
return (null, null, "Usuario no encontrado en la base de datos.");
|
return (null, null, "Usuario no encontrado en la base de datos.");
|
||||||
}
|
}
|
||||||
if (persona.ADMINISTRARPTYREGISTRO != true)
|
if (string.Equals(origen, "Registro", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& persona.ADMINISTRARPTYREGISTRO != true)
|
||||||
{
|
{
|
||||||
return (null, null, "Usuario no autorizado.");
|
return (null, null, "Usuario no autorizado.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,7 @@ namespace SwaggerAntifraude.DTOs
|
|||||||
public class CertificateProxyLoginDto
|
public class CertificateProxyLoginDto
|
||||||
{
|
{
|
||||||
public string Dni { get; set; } = string.Empty;
|
public string Dni { get; set; } = string.Empty;
|
||||||
|
public string? Origen { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user