using FastEndpoints; using Microsoft.EntityFrameworkCore; using Socialize.Api.Data; using Socialize.Api.Infrastructure.Security; using Socialize.Api.Modules.Channels.Data; namespace Socialize.Api.Modules.Channels.Handlers; internal record UpdateChannelRequest( Guid Id, Guid WorkspaceId, string Name, string Network, string? Handle, string? ExternalUrl, string? PortraitUrl, string? BannerUrl); internal class UpdateChannelRequestValidator : Validator { private static readonly string[] AllowedNetworks = [ "Instagram", "TikTok", "Facebook", "LinkedIn", "YouTube", "X", "Reddit", "Website", ]; public UpdateChannelRequestValidator() { RuleFor(x => x.Id).NotEmpty(); RuleFor(x => x.WorkspaceId).NotEmpty(); RuleFor(x => x.Name).NotEmpty().MaximumLength(256); RuleFor(x => x.Network).NotEmpty().Must(network => AllowedNetworks.Contains(network)) .WithMessage("Selected network is invalid."); RuleFor(x => x.Handle).MaximumLength(256); RuleFor(x => x.ExternalUrl).MaximumLength(2048); RuleFor(x => x.PortraitUrl).MaximumLength(2048); RuleFor(x => x.BannerUrl).MaximumLength(2048); } } internal class UpdateChannelHandler( AppDbContext dbContext, AccessScopeService accessScopeService) : Endpoint { public override void Configure() { Put("/api/channels/{id}"); Options(o => o.WithTags("Channels")); } public override async Task HandleAsync(UpdateChannelRequest request, CancellationToken ct) { Channel? channel = await dbContext.Channels .SingleOrDefaultAsync(candidate => candidate.Id == request.Id && candidate.WorkspaceId == request.WorkspaceId, ct); if (channel is null) { await SendNotFoundAsync(ct); return; } if (!await accessScopeService.CanManageWorkspaceAsync(User, channel.WorkspaceId, ct)) { await SendForbiddenAsync(ct); return; } string normalizedName = request.Name.Trim(); string normalizedNetwork = request.Network.Trim(); string? normalizedHandle = request.Handle?.Trim(); string? normalizedExternalUrl = request.ExternalUrl?.Trim(); string? normalizedPortraitUrl = request.PortraitUrl?.Trim(); string? normalizedBannerUrl = request.BannerUrl?.Trim(); bool duplicateChannel = await dbContext.Channels .AnyAsync( candidate => candidate.Id != channel.Id && candidate.WorkspaceId == channel.WorkspaceId && candidate.Network == normalizedNetwork && candidate.Name == normalizedName, ct); if (duplicateChannel) { AddError(request => request.Name, "A channel with this name already exists for the selected network."); await SendErrorsAsync(StatusCodes.Status409Conflict, ct); return; } channel.Name = normalizedName; channel.Network = normalizedNetwork; channel.Handle = string.IsNullOrWhiteSpace(normalizedHandle) ? null : normalizedHandle; channel.ExternalUrl = string.IsNullOrWhiteSpace(normalizedExternalUrl) ? null : normalizedExternalUrl; channel.PortraitUrl = string.IsNullOrWhiteSpace(normalizedPortraitUrl) ? null : normalizedPortraitUrl; channel.BannerUrl = string.IsNullOrWhiteSpace(normalizedBannerUrl) ? null : normalizedBannerUrl; await dbContext.SaveChangesAsync(ct); ChannelDto dto = new( channel.Id, channel.WorkspaceId, channel.Name, channel.Network, channel.Handle, channel.ExternalUrl, channel.PortraitUrl, channel.BannerUrl, channel.CreatedAt); await SendOkAsync(dto, ct); } }