Add real workspace channels
This commit is contained in:
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.Channels.Data;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
@@ -22,6 +23,7 @@ public class AppDbContext(
|
||||
public DbSet<OrganizationMembership> OrganizationMemberships => Set<OrganizationMembership>();
|
||||
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
||||
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
||||
public DbSet<Channel> Channels => Set<Channel>();
|
||||
public DbSet<Client> Clients => Set<Client>();
|
||||
public DbSet<Campaign> Campaigns => Set<Campaign>();
|
||||
public DbSet<ContentItem> ContentItems => Set<ContentItem>();
|
||||
@@ -46,6 +48,7 @@ public class AppDbContext(
|
||||
|
||||
builder.ConfigureOrganizationsModule();
|
||||
builder.ConfigureWorkspacesModule();
|
||||
builder.ConfigureChannelsModule();
|
||||
builder.ConfigureClientsModule();
|
||||
builder.ConfigureCampaignsModule();
|
||||
builder.ConfigureContentItemsModule();
|
||||
|
||||
@@ -6,6 +6,7 @@ using Socialize.Api.Modules.Identity.Contracts;
|
||||
using Socialize.Api.Modules.Identity.Data;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Channels.Data;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
@@ -23,10 +24,14 @@ public static class DevelopmentSeedExtensions
|
||||
{
|
||||
private static readonly Guid OrganizationId = Guid.Parse("99999999-9999-9999-9999-999999999999");
|
||||
private static readonly Guid WorkspaceId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
private static readonly Guid AtlasWorkspaceId = Guid.Parse("11111111-1111-1111-1111-222222222222");
|
||||
private static readonly Guid ScopedClientId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
||||
private static readonly Guid HiddenClientId = Guid.Parse("22222222-2222-2222-2222-333333333333");
|
||||
private static readonly Guid ScopedCampaignId = Guid.Parse("33333333-3333-3333-3333-333333333333");
|
||||
private static readonly Guid HiddenCampaignId = Guid.Parse("33333333-3333-3333-3333-444444444444");
|
||||
private static readonly Guid LumaInstagramChannelId = Guid.Parse("33333333-3333-3333-3333-000000000001");
|
||||
private static readonly Guid LumaTikTokChannelId = Guid.Parse("33333333-3333-3333-3333-000000000002");
|
||||
private static readonly Guid AtlasInstagramChannelId = Guid.Parse("33333333-3333-3333-3333-000000000003");
|
||||
private static readonly Guid ScopedContentItemId = Guid.Parse("44444444-4444-4444-4444-444444444444");
|
||||
private static readonly Guid HiddenContentItemId = Guid.Parse("44444444-4444-4444-4444-555555555555");
|
||||
private static readonly Guid ScopedAssetId = Guid.Parse("55555555-5555-5555-5555-555555555555");
|
||||
@@ -252,7 +257,7 @@ public static class DevelopmentSeedExtensions
|
||||
dbContext.Organizations.Add(organization);
|
||||
}
|
||||
|
||||
organization.Name = "Northstar Collective";
|
||||
organization.Name = "Northstar Agency";
|
||||
organization.OwnerUserId = managerUserId;
|
||||
|
||||
await UpsertOrganizationMembershipAsync(
|
||||
@@ -309,32 +314,31 @@ public static class DevelopmentSeedExtensions
|
||||
AppDbContext dbContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Workspace? workspace = await dbContext.Workspaces
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == WorkspaceId, cancellationToken);
|
||||
if (workspace is null)
|
||||
{
|
||||
workspace = new Workspace
|
||||
{
|
||||
Id = WorkspaceId,
|
||||
Name = string.Empty,
|
||||
TimeZone = string.Empty,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
dbContext.Workspaces.Add(workspace);
|
||||
}
|
||||
|
||||
workspace.Name = "Northstar Studio";
|
||||
workspace.OrganizationId = OrganizationId;
|
||||
workspace.OwnerUserId = managerUserId;
|
||||
workspace.TimeZone = "America/Montreal";
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await UpsertWorkspaceAsync(
|
||||
dbContext,
|
||||
WorkspaceId,
|
||||
OrganizationId,
|
||||
managerUserId,
|
||||
"Luma Coffee",
|
||||
"America/Montreal",
|
||||
"/images/seed/luma-coffee-logo.svg",
|
||||
cancellationToken);
|
||||
await UpsertWorkspaceAsync(
|
||||
dbContext,
|
||||
AtlasWorkspaceId,
|
||||
OrganizationId,
|
||||
managerUserId,
|
||||
"Atlas Bakery",
|
||||
"America/Montreal",
|
||||
"/images/seed/atlas-bakery-logo.svg",
|
||||
cancellationToken);
|
||||
|
||||
await UpsertClientAsync(
|
||||
dbContext,
|
||||
ScopedClientId,
|
||||
"Luma Coffee",
|
||||
"Active",
|
||||
"https://images.unsplash.com/photo-1511920170033-f8396924c348?auto=format&fit=crop&w=200&q=80",
|
||||
"/images/seed/luma-coffee-logo.svg",
|
||||
"Sofia Martin",
|
||||
"client@socialize.local",
|
||||
WorkspaceId,
|
||||
@@ -344,10 +348,10 @@ public static class DevelopmentSeedExtensions
|
||||
HiddenClientId,
|
||||
"Atlas Bakery",
|
||||
"Active",
|
||||
"https://images.unsplash.com/photo-1509440159596-0249088772ff?auto=format&fit=crop&w=200&q=80",
|
||||
"/images/seed/atlas-bakery-logo.svg",
|
||||
"Nina Cole",
|
||||
"nina@atlasbakery.test",
|
||||
WorkspaceId,
|
||||
AtlasWorkspaceId,
|
||||
cancellationToken);
|
||||
|
||||
await UpsertCampaignAsync(
|
||||
@@ -365,7 +369,7 @@ public static class DevelopmentSeedExtensions
|
||||
await UpsertCampaignAsync(
|
||||
dbContext,
|
||||
HiddenCampaignId,
|
||||
WorkspaceId,
|
||||
AtlasWorkspaceId,
|
||||
HiddenClientId,
|
||||
"Summer Retention",
|
||||
"Planned",
|
||||
@@ -375,6 +379,34 @@ public static class DevelopmentSeedExtensions
|
||||
"Sequence email and paid social updates together.",
|
||||
cancellationToken);
|
||||
|
||||
await UpsertChannelAsync(
|
||||
dbContext,
|
||||
LumaInstagramChannelId,
|
||||
WorkspaceId,
|
||||
"Luma Coffee Instagram",
|
||||
"Instagram",
|
||||
"@lumacoffee",
|
||||
null,
|
||||
cancellationToken);
|
||||
await UpsertChannelAsync(
|
||||
dbContext,
|
||||
LumaTikTokChannelId,
|
||||
WorkspaceId,
|
||||
"Luma Coffee TikTok",
|
||||
"TikTok",
|
||||
"@lumacoffee",
|
||||
null,
|
||||
cancellationToken);
|
||||
await UpsertChannelAsync(
|
||||
dbContext,
|
||||
AtlasInstagramChannelId,
|
||||
AtlasWorkspaceId,
|
||||
"Atlas Bakery Instagram",
|
||||
"Instagram",
|
||||
"@atlasbakery",
|
||||
null,
|
||||
cancellationToken);
|
||||
|
||||
await UpsertContentItemAsync(
|
||||
dbContext,
|
||||
ScopedContentItemId,
|
||||
@@ -383,7 +415,7 @@ public static class DevelopmentSeedExtensions
|
||||
ScopedCampaignId,
|
||||
"Spring launch hero video",
|
||||
"Fresh seasonal menu launch across Instagram and TikTok.",
|
||||
"Instagram Reel, TikTok",
|
||||
"Luma Coffee Instagram, Luma Coffee TikTok",
|
||||
"In approval",
|
||||
DateTimeOffset.UtcNow.AddDays(3),
|
||||
"v3",
|
||||
@@ -392,22 +424,22 @@ public static class DevelopmentSeedExtensions
|
||||
await UpsertContentItemAsync(
|
||||
dbContext,
|
||||
HiddenContentItemId,
|
||||
WorkspaceId,
|
||||
AtlasWorkspaceId,
|
||||
HiddenClientId,
|
||||
HiddenCampaignId,
|
||||
"Bakery loyalty carousel",
|
||||
"Reward regular customers with a four-card retention carousel.",
|
||||
"Instagram Carousel",
|
||||
"Atlas Bakery Instagram",
|
||||
"Draft",
|
||||
DateTimeOffset.UtcNow.AddDays(10),
|
||||
"v1",
|
||||
1,
|
||||
cancellationToken);
|
||||
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000001"), ScopedContentItemId, 1, "v1", "Spring launch hero video", "Initial draft for the seasonal menu launch.", "Instagram Reel, TikTok", "Initial concept draft.", providerUserId, DateTimeOffset.UtcNow.AddDays(-5), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000002"), ScopedContentItemId, 2, "v2", "Spring launch hero video", "Updated hook and transitions after internal review.", "Instagram Reel, TikTok", "Addressed internal pacing feedback.", providerUserId, DateTimeOffset.UtcNow.AddDays(-3), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000003"), ScopedContentItemId, 3, "v3", "Spring launch hero video", "Fresh seasonal menu launch across Instagram and TikTok.", "Instagram Reel, TikTok", "Client-facing draft after copy cleanup.", providerUserId, DateTimeOffset.UtcNow.AddDays(-1), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000004"), HiddenContentItemId, 1, "v1", "Bakery loyalty carousel", "Reward regular customers with a four-card retention carousel.", "Instagram Carousel", "First draft.", managerUserId, DateTimeOffset.UtcNow.AddDays(-2), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000001"), ScopedContentItemId, 1, "v1", "Spring launch hero video", "Initial draft for the seasonal menu launch.", "Luma Coffee Instagram, Luma Coffee TikTok", "Initial concept draft.", providerUserId, DateTimeOffset.UtcNow.AddDays(-5), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000002"), ScopedContentItemId, 2, "v2", "Spring launch hero video", "Updated hook and transitions after internal review.", "Luma Coffee Instagram, Luma Coffee TikTok", "Addressed internal pacing feedback.", providerUserId, DateTimeOffset.UtcNow.AddDays(-3), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000003"), ScopedContentItemId, 3, "v3", "Spring launch hero video", "Fresh seasonal menu launch across Instagram and TikTok.", "Luma Coffee Instagram, Luma Coffee TikTok", "Client-facing draft after copy cleanup.", providerUserId, DateTimeOffset.UtcNow.AddDays(-1), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000004"), HiddenContentItemId, 1, "v1", "Bakery loyalty carousel", "Reward regular customers with a four-card retention carousel.", "Atlas Bakery Instagram", "First draft.", managerUserId, DateTimeOffset.UtcNow.AddDays(-2), cancellationToken);
|
||||
|
||||
Asset? asset = await dbContext.Assets.SingleOrDefaultAsync(candidate => candidate.Id == ScopedAssetId, cancellationToken);
|
||||
if (asset is null)
|
||||
@@ -535,6 +567,38 @@ public static class DevelopmentSeedExtensions
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task UpsertWorkspaceAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid id,
|
||||
Guid organizationId,
|
||||
Guid ownerUserId,
|
||||
string name,
|
||||
string timeZone,
|
||||
string logoUrl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Workspace? workspace = await dbContext.Workspaces
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
|
||||
if (workspace is null)
|
||||
{
|
||||
workspace = new Workspace
|
||||
{
|
||||
Id = id,
|
||||
Name = string.Empty,
|
||||
TimeZone = string.Empty,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
dbContext.Workspaces.Add(workspace);
|
||||
}
|
||||
|
||||
workspace.Name = name;
|
||||
workspace.OrganizationId = organizationId;
|
||||
workspace.OwnerUserId = ownerUserId;
|
||||
workspace.TimeZone = timeZone;
|
||||
workspace.LogoUrl = logoUrl;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task UpsertClientAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid id,
|
||||
@@ -604,6 +668,37 @@ public static class DevelopmentSeedExtensions
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task UpsertChannelAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid id,
|
||||
Guid workspaceId,
|
||||
string name,
|
||||
string network,
|
||||
string? handle,
|
||||
string? externalUrl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Channel? channel = await dbContext.Channels.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
|
||||
if (channel is null)
|
||||
{
|
||||
channel = new Channel
|
||||
{
|
||||
Id = id,
|
||||
Name = string.Empty,
|
||||
Network = string.Empty,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
dbContext.Channels.Add(channel);
|
||||
}
|
||||
|
||||
channel.WorkspaceId = workspaceId;
|
||||
channel.Name = name;
|
||||
channel.Network = network;
|
||||
channel.Handle = handle;
|
||||
channel.ExternalUrl = externalUrl;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task UpsertContentItemAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid id,
|
||||
|
||||
1540
backend/src/Socialize.Api/Migrations/20260505162446_AddChannels.Designer.cs
generated
Normal file
1540
backend/src/Socialize.Api/Migrations/20260505162446_AddChannels.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddChannels : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Channels",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Network = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Handle = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ExternalUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Channels", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Channels_WorkspaceId",
|
||||
table: "Channels",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Channels_WorkspaceId_Network_Name",
|
||||
table: "Channels",
|
||||
columns: new[] { "WorkspaceId", "Network", "Name" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Channels");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -491,6 +491,48 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("Campaigns", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Channels.Data.Channel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("ExternalUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Handle")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Network")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.HasIndex("WorkspaceId", "Network", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Channels", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ using Socialize.Api.Infrastructure;
|
||||
using Socialize.Api.Infrastructure.Development;
|
||||
using Socialize.Api.Modules.Approvals;
|
||||
using Socialize.Api.Modules.Assets;
|
||||
using Socialize.Api.Modules.Channels;
|
||||
using Socialize.Api.Modules.Clients;
|
||||
using Socialize.Api.Modules.Comments;
|
||||
using Socialize.Api.Modules.ContentItems;
|
||||
@@ -65,6 +66,7 @@ builder.AddInfrastructureModule();
|
||||
builder.AddIdentityModule();
|
||||
builder.AddOrganizationsModule();
|
||||
builder.AddWorkspaceModule();
|
||||
builder.AddChannelsModule();
|
||||
builder.AddClientsModule();
|
||||
builder.AddCampaignsModule();
|
||||
builder.AddContentItemsModule();
|
||||
|
||||
Reference in New Issue
Block a user