wip
This commit is contained in:
@@ -2,21 +2,52 @@ using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Approvals.Services;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
||||
|
||||
public record UpdateApprovalStepConfigurationRequest(
|
||||
string Name,
|
||||
int SortOrder,
|
||||
string TargetType,
|
||||
string TargetValue,
|
||||
int RequiredApproverCount);
|
||||
|
||||
public record UpdateWorkspaceRequest(
|
||||
string Name,
|
||||
string TimeZone);
|
||||
string TimeZone,
|
||||
string? ApprovalMode,
|
||||
bool? SchedulePostsAutomaticallyOnApproval,
|
||||
bool? LockContentAfterApproval,
|
||||
bool? SendAutomaticApprovalReminders,
|
||||
IReadOnlyCollection<UpdateApprovalStepConfigurationRequest>? ApprovalSteps);
|
||||
|
||||
public class UpdateWorkspaceRequestValidator
|
||||
: Validator<UpdateWorkspaceRequest>
|
||||
{
|
||||
private static readonly string[] AllowedApprovalModes = ["None", "Optional", "Required", "Multi-level"];
|
||||
|
||||
public UpdateWorkspaceRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(128);
|
||||
RuleFor(x => x.ApprovalMode)
|
||||
.Must(mode => string.IsNullOrWhiteSpace(mode) || AllowedApprovalModes.Contains(mode.Trim()))
|
||||
.WithMessage("A valid approval mode should be specified.");
|
||||
RuleFor(x => x.ApprovalSteps)
|
||||
.Must(steps => steps is null || steps.Select(step => step.SortOrder).Distinct().Count() == steps.Count)
|
||||
.WithMessage("Approval step sort orders must be unique.");
|
||||
RuleForEach(x => x.ApprovalSteps).ChildRules(step =>
|
||||
{
|
||||
step.RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
|
||||
step.RuleFor(x => x.TargetType)
|
||||
.Must(ApprovalStepConfigurationRules.IsValidTargetType)
|
||||
.WithMessage("A valid approval step target type should be specified.");
|
||||
step.RuleFor(x => x.TargetValue).NotEmpty().MaximumLength(128);
|
||||
step.RuleFor(x => x.RequiredApproverCount).GreaterThanOrEqualTo(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,19 +79,162 @@ public class UpdateWorkspaceHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
string nextApprovalMode = string.IsNullOrWhiteSpace(request.ApprovalMode)
|
||||
? workspace.ApprovalMode
|
||||
: request.ApprovalMode.Trim();
|
||||
List<UpdateApprovalStepConfigurationRequest>? requestedApprovalSteps = request.ApprovalSteps?.ToList();
|
||||
|
||||
if (nextApprovalMode == ApprovalModes.MultiLevel)
|
||||
{
|
||||
bool hasConfiguredSteps = requestedApprovalSteps is null
|
||||
? await dbContext.WorkspaceApprovalStepConfigurations.AnyAsync(step => step.WorkspaceId == workspace.Id, ct)
|
||||
: requestedApprovalSteps.Count > 0;
|
||||
|
||||
if (!hasConfiguredSteps)
|
||||
{
|
||||
AddError(request => request.ApprovalSteps, "Multi-level approval requires at least one approval step.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (requestedApprovalSteps is not null &&
|
||||
!await ValidateApprovalStepsAsync(workspace.Id, requestedApprovalSteps, ct))
|
||||
{
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
workspace.Name = request.Name.Trim();
|
||||
workspace.TimeZone = request.TimeZone.Trim();
|
||||
workspace.ApprovalMode = nextApprovalMode;
|
||||
workspace.SchedulePostsAutomaticallyOnApproval = request.SchedulePostsAutomaticallyOnApproval ?? workspace.SchedulePostsAutomaticallyOnApproval;
|
||||
workspace.LockContentAfterApproval = request.LockContentAfterApproval ?? workspace.LockContentAfterApproval;
|
||||
workspace.SendAutomaticApprovalReminders = request.SendAutomaticApprovalReminders ?? workspace.SendAutomaticApprovalReminders;
|
||||
|
||||
if (requestedApprovalSteps is not null)
|
||||
{
|
||||
List<WorkspaceApprovalStepConfiguration> existingSteps = await dbContext.WorkspaceApprovalStepConfigurations
|
||||
.Where(step => step.WorkspaceId == workspace.Id)
|
||||
.ToListAsync(ct);
|
||||
dbContext.WorkspaceApprovalStepConfigurations.RemoveRange(existingSteps);
|
||||
|
||||
List<WorkspaceApprovalStepConfiguration> replacementSteps = requestedApprovalSteps
|
||||
.OrderBy(step => step.SortOrder)
|
||||
.Select(step => new WorkspaceApprovalStepConfiguration
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
WorkspaceId = workspace.Id,
|
||||
Name = step.Name.Trim(),
|
||||
SortOrder = step.SortOrder,
|
||||
TargetType = step.TargetType.Trim(),
|
||||
TargetValue = NormalizeTargetValue(step),
|
||||
RequiredApproverCount = step.RequiredApproverCount,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
dbContext.WorkspaceApprovalStepConfigurations.AddRange(replacementSteps);
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
List<ApprovalStepConfigurationDto> approvalSteps = await dbContext.WorkspaceApprovalStepConfigurations
|
||||
.Where(step => step.WorkspaceId == workspace.Id)
|
||||
.OrderBy(step => step.SortOrder)
|
||||
.ThenBy(step => step.Name)
|
||||
.Select(step => new ApprovalStepConfigurationDto(
|
||||
step.Id,
|
||||
step.WorkspaceId,
|
||||
step.Name,
|
||||
step.SortOrder,
|
||||
step.TargetType,
|
||||
step.TargetValue,
|
||||
step.RequiredApproverCount,
|
||||
step.CreatedAt))
|
||||
.ToListAsync(ct);
|
||||
|
||||
WorkspaceDto dto = new(
|
||||
workspace.Id,
|
||||
workspace.Name,
|
||||
workspace.Slug,
|
||||
workspace.LogoUrl,
|
||||
workspace.TimeZone,
|
||||
workspace.ApprovalMode,
|
||||
workspace.SchedulePostsAutomaticallyOnApproval,
|
||||
workspace.LockContentAfterApproval,
|
||||
workspace.SendAutomaticApprovalReminders,
|
||||
approvalSteps,
|
||||
workspace.CreatedAt);
|
||||
|
||||
await SendOkAsync(dto, ct);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateApprovalStepsAsync(
|
||||
Guid workspaceId,
|
||||
IReadOnlyCollection<UpdateApprovalStepConfigurationRequest> steps,
|
||||
CancellationToken ct)
|
||||
{
|
||||
foreach (UpdateApprovalStepConfigurationRequest step in steps)
|
||||
{
|
||||
string targetType = step.TargetType.Trim();
|
||||
string targetValue = step.TargetValue.Trim();
|
||||
|
||||
if (targetType == ApprovalStepTargetTypes.Role &&
|
||||
!ApprovalStepConfigurationRules.IsValidRoleTarget(targetValue))
|
||||
{
|
||||
AddError(request => request.ApprovalSteps, $"'{targetValue}' is not a supported approval role target.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetType == ApprovalStepTargetTypes.Membership &&
|
||||
!ApprovalStepConfigurationRules.IsValidMembershipTarget(targetValue))
|
||||
{
|
||||
AddError(request => request.ApprovalSteps, $"'{targetValue}' is not a supported approval membership target.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetType == ApprovalStepTargetTypes.Member)
|
||||
{
|
||||
IReadOnlyCollection<Guid> memberUserIds = ApprovalWorkflowRules.ParseMemberTargetIds(targetValue);
|
||||
|
||||
if (memberUserIds.Count == 0)
|
||||
{
|
||||
AddError(request => request.ApprovalSteps, "Member approval step targets must reference at least one valid user id.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (memberUserIds.Count < step.RequiredApproverCount)
|
||||
{
|
||||
AddError(request => request.ApprovalSteps, "Member approval step targets must include at least as many members as required approvers.");
|
||||
return false;
|
||||
}
|
||||
|
||||
string workspaceClaimValue = workspaceId.ToString();
|
||||
int workspaceMemberCount = await dbContext.UserClaims
|
||||
.Where(claim => memberUserIds.Contains(claim.UserId) &&
|
||||
claim.ClaimType == KnownClaims.WorkspaceScope &&
|
||||
claim.ClaimValue == workspaceClaimValue)
|
||||
.Select(claim => claim.UserId)
|
||||
.Distinct()
|
||||
.CountAsync(ct);
|
||||
|
||||
if (workspaceMemberCount != memberUserIds.Count)
|
||||
{
|
||||
AddError(request => request.ApprovalSteps, "Member approval step targets must reference users with access to the workspace.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string NormalizeTargetValue(UpdateApprovalStepConfigurationRequest step)
|
||||
{
|
||||
string targetValue = step.TargetValue.Trim();
|
||||
return step.TargetType.Trim() == ApprovalStepTargetTypes.Member
|
||||
? ApprovalWorkflowRules.FormatMemberTargetValue(ApprovalWorkflowRules.ParseMemberTargetIds(targetValue))
|
||||
: targetValue;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user