Add real workspace channels
This commit is contained in:
12
backend/src/Socialize.Api/Modules/Channels/Data/Channel.cs
Normal file
12
backend/src/Socialize.Api/Modules/Channels/Data/Channel.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Socialize.Api.Modules.Channels.Data;
|
||||
|
||||
public class Channel
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid WorkspaceId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Network { get; set; }
|
||||
public string? Handle { get; set; }
|
||||
public string? ExternalUrl { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Socialize.Api.Modules.Channels.Data;
|
||||
|
||||
public static class ChannelModelConfiguration
|
||||
{
|
||||
public static ModelBuilder ConfigureChannelsModule(this ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Channel>(channel =>
|
||||
{
|
||||
channel.ToTable("Channels");
|
||||
channel.HasKey(x => x.Id);
|
||||
channel.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||
channel.Property(x => x.Network).HasMaxLength(64).IsRequired();
|
||||
channel.Property(x => x.Handle).HasMaxLength(256);
|
||||
channel.Property(x => x.ExternalUrl).HasMaxLength(2048);
|
||||
channel.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
channel.HasIndex(x => x.WorkspaceId);
|
||||
channel.HasIndex(x => new { x.WorkspaceId, x.Network, x.Name }).IsUnique();
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Socialize.Api.Modules.Channels;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static WebApplicationBuilder AddChannelsModule(
|
||||
this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Socialize.Api.Modules.Channels.Handlers;
|
||||
|
||||
public record ChannelDto(
|
||||
Guid Id,
|
||||
Guid WorkspaceId,
|
||||
string Name,
|
||||
string Network,
|
||||
string? Handle,
|
||||
string? ExternalUrl,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,115 @@
|
||||
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;
|
||||
|
||||
public record CreateChannelRequest(
|
||||
Guid WorkspaceId,
|
||||
string Name,
|
||||
string Network,
|
||||
string? Handle,
|
||||
string? ExternalUrl);
|
||||
|
||||
public class CreateChannelRequestValidator
|
||||
: Validator<CreateChannelRequest>
|
||||
{
|
||||
private static readonly string[] AllowedNetworks =
|
||||
[
|
||||
"Instagram",
|
||||
"TikTok",
|
||||
"Facebook",
|
||||
"LinkedIn",
|
||||
"YouTube",
|
||||
"X",
|
||||
"Reddit",
|
||||
"Website",
|
||||
];
|
||||
|
||||
public CreateChannelRequestValidator()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateChannelHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService)
|
||||
: Endpoint<CreateChannelRequest, ChannelDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/channels");
|
||||
Options(o => o.WithTags("Channels"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateChannelRequest request, CancellationToken ct)
|
||||
{
|
||||
if (!await accessScopeService.CanManageWorkspaceAsync(User, request.WorkspaceId, ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
bool workspaceExists = await dbContext.Workspaces
|
||||
.AnyAsync(workspace => workspace.Id == request.WorkspaceId, ct);
|
||||
|
||||
if (!workspaceExists)
|
||||
{
|
||||
AddError(request => request.WorkspaceId, "The selected workspace does not exist.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
string normalizedName = request.Name.Trim();
|
||||
string normalizedNetwork = request.Network.Trim();
|
||||
string? normalizedHandle = request.Handle?.Trim();
|
||||
string? normalizedExternalUrl = request.ExternalUrl?.Trim();
|
||||
|
||||
bool duplicateChannel = await dbContext.Channels
|
||||
.AnyAsync(
|
||||
channel => channel.WorkspaceId == request.WorkspaceId
|
||||
&& channel.Network == normalizedNetwork
|
||||
&& channel.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 channel = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
WorkspaceId = request.WorkspaceId,
|
||||
Name = normalizedName,
|
||||
Network = normalizedNetwork,
|
||||
Handle = string.IsNullOrWhiteSpace(normalizedHandle) ? null : normalizedHandle,
|
||||
ExternalUrl = string.IsNullOrWhiteSpace(normalizedExternalUrl) ? null : normalizedExternalUrl,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
dbContext.Channels.Add(channel);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
ChannelDto dto = new(
|
||||
channel.Id,
|
||||
channel.WorkspaceId,
|
||||
channel.Name,
|
||||
channel.Network,
|
||||
channel.Handle,
|
||||
channel.ExternalUrl,
|
||||
channel.CreatedAt);
|
||||
|
||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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;
|
||||
|
||||
public record GetChannelsRequest(Guid? WorkspaceId);
|
||||
|
||||
public class GetChannelsHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService)
|
||||
: Endpoint<GetChannelsRequest, IReadOnlyCollection<ChannelDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/channels");
|
||||
Options(o => o.WithTags("Channels"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetChannelsRequest request, CancellationToken ct)
|
||||
{
|
||||
IQueryable<Channel> query = dbContext.Channels.AsQueryable();
|
||||
|
||||
if (!accessScopeService.IsManager(User))
|
||||
{
|
||||
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||
query = query.Where(channel => workspaceScopeIds.Contains(channel.WorkspaceId));
|
||||
}
|
||||
|
||||
if (request.WorkspaceId.HasValue)
|
||||
{
|
||||
query = query.Where(channel => channel.WorkspaceId == request.WorkspaceId.Value);
|
||||
}
|
||||
|
||||
List<ChannelDto> channels = await query
|
||||
.OrderBy(channel => channel.Network)
|
||||
.ThenBy(channel => channel.Name)
|
||||
.Select(channel => new ChannelDto(
|
||||
channel.Id,
|
||||
channel.WorkspaceId,
|
||||
channel.Name,
|
||||
channel.Network,
|
||||
channel.Handle,
|
||||
channel.ExternalUrl,
|
||||
channel.CreatedAt))
|
||||
.ToListAsync(ct);
|
||||
|
||||
await SendOkAsync(channels, ct);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user