using System.Security.Cryptography; using Socialize.Infrastructure.Security; using Socialize.Modules.Notifications.Contracts; namespace Socialize.Modules.Approvals.Handlers; public record CreateApprovalRequestRequest( Guid WorkspaceId, Guid ContentItemId, string Stage, string ReviewerName, string ReviewerEmail, DateTimeOffset? DueAt); public class CreateApprovalRequestRequestValidator : Validator { public CreateApprovalRequestRequestValidator() { RuleFor(x => x.WorkspaceId).NotEmpty(); RuleFor(x => x.ContentItemId).NotEmpty(); RuleFor(x => x.Stage).NotEmpty().MaximumLength(64); RuleFor(x => x.ReviewerName).NotEmpty().MaximumLength(256); RuleFor(x => x.ReviewerEmail).NotEmpty().MaximumLength(256).EmailAddress(); } } public class CreateApprovalRequestHandler( AppDbContext dbContext, AccessScopeService accessScopeService, INotificationEventWriter notificationEventWriter) : Endpoint { public override void Configure() { Post("/api/approvals"); Options(o => o.WithTags("Approvals")); } public override async Task HandleAsync(CreateApprovalRequestRequest request, CancellationToken ct) { ContentItem? contentItem = await dbContext.ContentItems .SingleOrDefaultAsync( candidate => candidate.Id == request.ContentItemId && candidate.WorkspaceId == request.WorkspaceId, ct); if (contentItem is null) { AddError(request => request.ContentItemId, "The selected content item does not exist in the active workspace."); await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); return; } if (!accessScopeService.CanManageWorkspace(User, contentItem.WorkspaceId)) { await SendForbiddenAsync(ct); return; } ApprovalRequest approval = new() { Id = Guid.NewGuid(), WorkspaceId = request.WorkspaceId, ContentItemId = request.ContentItemId, Stage = request.Stage.Trim(), ReviewerName = request.ReviewerName.Trim(), ReviewerEmail = request.ReviewerEmail.Trim(), RequestedByUserId = User.GetUserId(), DueAt = request.DueAt, State = "Pending", AccessToken = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant(), SentAt = DateTimeOffset.UtcNow, }; dbContext.ApprovalRequests.Add(approval); if (approval.Stage == "Internal") { contentItem.Status = "In internal review"; } else if (approval.Stage == "Client") { contentItem.Status = "In client review"; } await dbContext.SaveChangesAsync(ct); await notificationEventWriter.WriteAsync( new NotificationEventWriteModel( approval.WorkspaceId, approval.ContentItemId, "approval.requested", "ApprovalRequest", approval.Id, $"Approval requested from {approval.ReviewerName} for {contentItem.Title}.", null, approval.ReviewerEmail, $$"""{"stage":"{{approval.Stage}}","accessToken":"{{approval.AccessToken}}"}"""), ct); ApprovalRequestDto dto = new( approval.Id, approval.WorkspaceId, approval.ContentItemId, approval.Stage, approval.ReviewerName, approval.ReviewerEmail, approval.RequestedByUserId, approval.DueAt, approval.State, approval.AccessToken, approval.SentAt, approval.CompletedAt, []); await SendAsync(dto, StatusCodes.Status201Created, ct); } }