diff --git a/backend/src/Socialize.Api/Migrations/20260509170908_AddChannelImages.Designer.cs b/backend/src/Socialize.Api/Migrations/20260509170908_AddChannelImages.Designer.cs new file mode 100644 index 00000000..28e7a04e --- /dev/null +++ b/backend/src/Socialize.Api/Migrations/20260509170908_AddChannelImages.Designer.cs @@ -0,0 +1,2605 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Socialize.Api.Data; + +#nullable disable + +namespace Socialize.Api.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260509170908_AddChannelImages")] + partial class AddChannelImages + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("RoleId") + .HasColumnType("uuid"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalDecision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApprovalRequestId") + .HasColumnType("uuid"); + + b.Property("Comment") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DecidedByEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DecidedByName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("DecidedByUserId") + .HasColumnType("uuid"); + + b.Property("Decision") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("ApprovalRequestId"); + + b.ToTable("ApprovalDecisions", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessToken") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("DueAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RequestedByUserId") + .HasColumnType("uuid"); + + b.Property("ReviewerEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ReviewerName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Stage") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("State") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkflowInstanceId") + .HasColumnType("uuid"); + + b.Property("WorkflowStepRequiredApproverCount") + .HasColumnType("integer"); + + b.Property("WorkflowStepSortOrder") + .HasColumnType("integer"); + + b.Property("WorkflowStepTargetType") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("WorkflowStepTargetValue") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("ReviewerEmail"); + + b.HasIndex("WorkflowInstanceId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("ApprovalRequests", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApprovalMode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("StartedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("State") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("ContentItemId", "State") + .IsUnique() + .HasFilter("\"State\" = 'Pending'"); + + b.ToTable("ApprovalWorkflowInstances", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.WorkspaceApprovalStepConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("RequiredApproverCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(1); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("TargetValue") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("WorkspaceId", "SortOrder") + .IsUnique(); + + b.ToTable("WorkspaceApprovalStepConfigurations", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssetType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CurrentRevisionNumber") + .HasColumnType("integer"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GoogleDriveFileId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GoogleDriveLink") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("PreviewUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("Assets", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.AssetRevision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssetId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("PreviewUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("RevisionNumber") + .HasColumnType("integer"); + + b.Property("SourceReference") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.HasKey("Id"); + + b.HasIndex("AssetId"); + + b.HasIndex("AssetId", "RevisionNumber") + .IsUnique(); + + b.ToTable("AssetRevisions", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarCatalogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Country") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CultureOrReligion") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("DefaultColor") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("Language") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Region") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("SourceUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("TrustLevel") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("Category"); + + b.HasIndex("Country"); + + b.HasIndex("ProviderName"); + + b.ToTable("CalendarCatalogEntries", (string)null); + + b.HasData( + new + { + Id = new Guid("10000000-0000-0000-0000-000000000001"), + Category = "public-holiday", + Country = "US", + CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DefaultColor = "#2F80ED", + Description = "Federal public holiday calendar for the United States.", + Language = "en", + ProviderName = "Nager.Date", + SourceUrl = "https://date.nager.at/api/v3/PublicHolidays/2026/US", + Title = "United States Public Holidays", + TrustLevel = "Verified" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000002"), + Category = "public-holiday", + Country = "CA", + CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DefaultColor = "#2F80ED", + Description = "Public holiday calendar for Canada.", + Language = "en", + ProviderName = "Nager.Date", + SourceUrl = "https://date.nager.at/api/v3/PublicHolidays/2026/CA", + Title = "Canada Public Holidays", + TrustLevel = "Verified" + }, + new + { + Id = new Guid("10000000-0000-0000-0000-000000000003"), + Category = "marketing-moment", + CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + DefaultColor = "#9B51E0", + Description = "Common retail, awareness, and social planning moments.", + Language = "en", + ProviderName = "Socialize", + SourceUrl = "https://example.com/socialize/marketing-moments.ics", + Title = "Common Marketing Moments", + TrustLevel = "Maintained" + }); + }); + + modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CalendarSourceId") + .HasColumnType("uuid"); + + b.Property("Description") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("EndLocalDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EndUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("ImportedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsAllDay") + .HasColumnType("boolean"); + + b.Property("IsFloatingTime") + .HasColumnType("boolean"); + + b.Property("Location") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RecurrenceId") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("SourceEventUid") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("SourceLastModifiedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("StartLocalDateTime") + .HasColumnType("timestamp with time zone"); + + b.Property("StartUtc") + .HasColumnType("timestamp with time zone"); + + b.Property("TimeZoneId") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Id"); + + b.HasIndex("CalendarSourceId"); + + b.HasIndex("CalendarSourceId", "SourceEventUid", "StartDate") + .IsUnique(); + + b.ToTable("CalendarEvents", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarSource", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CatalogSourceReference") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Color") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DisplayTitle") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("InheritanceMode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("LastAttemptedSyncAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSuccessfulSyncAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSyncError") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("SourceUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("Scope"); + + b.HasIndex("UserId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("CalendarSources", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.UserCalendarExportFeed", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("RevokedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Token") + .HasMaxLength(96) + .HasColumnType("character varying(96)"); + + b.Property("TokenHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UpdatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserCalendarExportFeeds", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Notes") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("ClientId", "Name") + .IsUnique(); + + b.ToTable("Campaigns", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Channels.Data.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BannerUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ExternalUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Handle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Network") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PortraitUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PortraitUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("PrimaryContactEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PrimaryContactName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PrimaryContactPortraitUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("WorkspaceId", "Name") + .IsUnique(); + + b.ToTable("Clients", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Comments.Data.Comment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttachmentBlobContainerName") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("AttachmentBlobName") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("AttachmentBlobUrl") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("AttachmentContentType") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("AttachmentFileName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AttachmentSizeBytes") + .HasColumnType("bigint"); + + b.Property("AuthorDisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AuthorEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AuthorUserId") + .HasColumnType("uuid"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ParentCommentId") + .HasColumnType("uuid"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("ParentCommentId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("Comments", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CurrentRevisionLabel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("CurrentRevisionNumber") + .HasColumnType("integer"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Hashtags") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("PublicationMessage") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PublicationTargets") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("ClientId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("ContentItems", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemActivityEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActorEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ActorUserId") + .HasColumnType("uuid"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("EntityId") + .HasColumnType("uuid"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("MetadataJson") + .HasColumnType("jsonb"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("ContentItemId", "CreatedAt"); + + b.ToTable("ContentItemActivityEntries", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ChangeSummary") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("Hashtags") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("PublicationMessage") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("PublicationTargets") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("RevisionLabel") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("RevisionNumber") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("ContentItemId", "RevisionNumber") + .IsUnique(); + + b.ToTable("ContentItemRevisions", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActivityType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ActorDisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ActorEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ActorUserId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("FeedbackReportId") + .HasColumnType("uuid"); + + b.Property("FromValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Note") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("ToValue") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasKey("Id"); + + b.HasIndex("ActorUserId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FeedbackReportId"); + + b.ToTable("FeedbackActivityEntries", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AuthorDisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AuthorEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AuthorRole") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("AuthorUserId") + .HasColumnType("uuid"); + + b.Property("Body") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("FeedbackReportId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("AuthorUserId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("FeedbackReportId"); + + b.ToTable("FeedbackComments", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppVersion") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("BrowserUserAgent") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("CampaignId") + .HasColumnType("uuid"); + + b.Property("CampaignName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CancellationReason") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("CancelledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CancelledByUserId") + .HasColumnType("uuid"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("ContentItemTitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(8000) + .HasColumnType("character varying(8000)"); + + b.Property("LastActivityAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReporterDisplayName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ReporterEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ReporterUserId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("SubmittedPath") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("ViewportHeight") + .HasColumnType("integer"); + + b.Property("ViewportWidth") + .HasColumnType("integer"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.Property("WorkspaceName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("CampaignId"); + + b.HasIndex("ClientId"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("LastActivityAt"); + + b.HasIndex("ReporterUserId"); + + b.HasIndex("Status"); + + b.HasIndex("Type"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("FeedbackReports", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BlobContainerName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("BlobName") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("FeedbackReportId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FeedbackReportId") + .IsUnique(); + + b.ToTable("FeedbackScreenshots", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("FeedbackReportId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName"); + + b.HasIndex("FeedbackReportId", "NormalizedName") + .IsUnique(); + + b.ToTable("FeedbackTags", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Identity.Data.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Identity.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("Address") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Alias") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BirthDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FacebookId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Firstname") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GoogleId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastAuthenticatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Lastname") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("PortraitUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("PreferredLanguage") + .IsRequired() + .HasMaxLength(8) + .HasColumnType("character varying(8)"); + + b.Property("RefreshToken") + .HasMaxLength(44) + .HasColumnType("character varying(44)"); + + b.Property("RefreshTokenExpiryTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Notifications.Data.NotificationEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContentItemId") + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("EntityId") + .HasColumnType("uuid"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("MetadataJson") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ReadAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RecipientEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RecipientUserId") + .HasColumnType("uuid"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ContentItemId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("RecipientUserId"); + + b.HasIndex("WorkspaceId"); + + b.ToTable("NotificationEvents", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LogoUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("MembershipTierId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValue(new Guid("20000000-0000-0000-0000-000000000001")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OwnerUserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MembershipTierId"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Organizations", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique(); + + b.ToTable("OrganizationMemberships", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ActiveContentLimit") + .HasColumnType("integer"); + + b.Property("ExternalReviewerLimit") + .HasColumnType("integer"); + + b.Property("IsCustom") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("MemberLimit") + .HasColumnType("integer"); + + b.Property("MonthlyPriceCents") + .HasColumnType("integer"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.Property("WorkspaceLimit") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.HasIndex("SortOrder"); + + b.ToTable("OrganizationMembershipTiers", (string)null); + + b.HasData( + new + { + Id = new Guid("20000000-0000-0000-0000-000000000001"), + ActiveContentLimit = 3, + ExternalReviewerLimit = 1, + IsCustom = false, + Key = "free", + MemberLimit = 2, + MonthlyPriceCents = 0, + SortOrder = 10, + WorkspaceLimit = 1 + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000002"), + ActiveContentLimit = 25, + ExternalReviewerLimit = 10, + IsCustom = false, + Key = "freelance", + MemberLimit = 5, + MonthlyPriceCents = 1900, + SortOrder = 20, + WorkspaceLimit = 3 + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000003"), + ActiveContentLimit = 250, + IsCustom = false, + Key = "agency", + MemberLimit = 25, + MonthlyPriceCents = 7900, + SortOrder = 30, + WorkspaceLimit = 15 + }, + new + { + Id = new Guid("20000000-0000-0000-0000-000000000004"), + IsCustom = true, + Key = "enterprise", + SortOrder = 40 + }); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTierTranslation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("MembershipTierId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("MembershipTierId", "Culture") + .IsUnique(); + + b.ToTable("OrganizationMembershipTierTranslations", (string)null); + + b.HasData( + new + { + Id = new Guid("20000000-0000-0001-0000-000000000001"), + Culture = "en", + Description = "For trying Socialize on one real approval workflow.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000001"), + Name = "Free" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000002"), + Culture = "fr", + Description = "Pour essayer Socialize sur un vrai workflow d'approbation.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000001"), + Name = "Free" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000003"), + Culture = "en", + Description = "For solo operators managing recurring client reviews.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000002"), + Name = "Freelance" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000004"), + Culture = "fr", + Description = "Pour les independants qui gerent des revisions client recurrentes.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000002"), + Name = "Freelance" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000005"), + Culture = "en", + Description = "For agencies that need repeatable client approval operations.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000003"), + Name = "Agency" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000006"), + Culture = "fr", + Description = "Pour les agences qui veulent des operations d'approbation client repetables.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000003"), + Name = "Agency" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000007"), + Culture = "en", + Description = "For larger organizations with governance and access needs.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000004"), + Name = "Enterprise" + }, + new + { + Id = new Guid("20000000-0000-0001-0000-000000000008"), + Culture = "fr", + Description = "Pour les grandes organisations avec des besoins de gouvernance et d'acces.", + MembershipTierId = new Guid("20000000-0000-0000-0000-000000000004"), + Name = "Enterprise" + }); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseCommit", b => + { + b.Property("Sha") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("AuthorEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AuthorName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("AuthoredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CommittedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CommunicationStatus") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("DeploymentLabel") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ExternalUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("ImportedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ReleaseUpdateId") + .HasColumnType("uuid"); + + b.Property("ShortSha") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)"); + + b.Property("SourceBranch") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Sha"); + + b.HasIndex("CommittedAt"); + + b.HasIndex("CommunicationStatus"); + + b.HasIndex("ReleaseUpdateId"); + + b.ToTable("ReleaseCommits", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ArchivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedByUserId") + .HasColumnType("uuid"); + + b.Property("PublishedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("Summary") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("SummaryFr") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("TitleFr") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("character varying(160)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedByUserId"); + + b.HasIndex("PublishedAt"); + + b.HasIndex("Status"); + + b.ToTable("ReleaseUpdates", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdateEmailDigestReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SentAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UpdateCount") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("SentAt"); + + b.HasIndex("UserId"); + + b.ToTable("ReleaseUpdateEmailDigestReceipts", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdateReadReceipt", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ReadAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ReleaseUpdateId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ReleaseUpdateId", "UserId") + .IsUnique(); + + b.ToTable("ReleaseUpdateReadReceipts", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApprovalMode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasDefaultValue("Required"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("LockContentAfterApproval") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("LogoUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OwnerUserId") + .HasColumnType("uuid"); + + b.Property("SchedulePostsAutomaticallyOnApproval") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("SendAutomaticApprovalReminders") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("TimeZone") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("Workspaces", (string)null); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.WorkspaceInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("InvitedByUserId") + .HasColumnType("uuid"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("WorkspaceId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("WorkspaceId"); + + b.HasIndex("WorkspaceId", "Email", "Status"); + + b.ToTable("WorkspaceInvites", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Socialize.Api.Modules.Identity.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Socialize.Api.Modules.Identity.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Socialize.Api.Modules.Identity.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Socialize.Api.Modules.Identity.Data.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Identity.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Socialize.Api.Modules.Identity.Data.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalDecision", b => + { + b.HasOne("Socialize.Api.Modules.Approvals.Data.ApprovalRequest", null) + .WithMany() + .HasForeignKey("ApprovalRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalRequest", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", null) + .WithMany() + .HasForeignKey("WorkflowInstanceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.WorkspaceApprovalStepConfiguration", b => + { + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.AssetRevision", b => + { + b.HasOne("Socialize.Api.Modules.Assets.Data.Asset", null) + .WithMany() + .HasForeignKey("AssetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarEvent", b => + { + b.HasOne("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarSource", null) + .WithMany() + .HasForeignKey("CalendarSourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b => + { + b.HasOne("Socialize.Api.Modules.Clients.Data.Client", null) + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Channels.Data.Channel", b => + { + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b => + { + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Comments.Data.Comment", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Comments.Data.Comment", null) + .WithMany() + .HasForeignKey("ParentCommentId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItem", b => + { + b.HasOne("Socialize.Api.Modules.Campaigns.Data.Campaign", null) + .WithMany() + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Clients.Data.Client", null) + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemActivityEntry", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b => + { + b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport") + .WithMany("ActivityEntries") + .HasForeignKey("FeedbackReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FeedbackReport"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackComment", b => + { + b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport") + .WithMany("Comments") + .HasForeignKey("FeedbackReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FeedbackReport"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b => + { + b.HasOne("Socialize.Api.Modules.Campaigns.Data.Campaign", null) + .WithMany() + .HasForeignKey("CampaignId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Socialize.Api.Modules.Clients.Data.Client", null) + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", b => + { + b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport") + .WithOne("Screenshot") + .HasForeignKey("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", "FeedbackReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FeedbackReport"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackTag", b => + { + b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport") + .WithMany("Tags") + .HasForeignKey("FeedbackReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FeedbackReport"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Notifications.Data.NotificationEvent", b => + { + b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null) + .WithMany() + .HasForeignKey("ContentItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.Organization", b => + { + b.HasOne("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", null) + .WithMany() + .HasForeignKey("MembershipTierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b => + { + b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTierTranslation", b => + { + b.HasOne("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", null) + .WithMany() + .HasForeignKey("MembershipTierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseCommit", b => + { + b.HasOne("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdate", "ReleaseUpdate") + .WithMany() + .HasForeignKey("ReleaseUpdateId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("ReleaseUpdate"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdateReadReceipt", b => + { + b.HasOne("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdate", "ReleaseUpdate") + .WithMany("ReadReceipts") + .HasForeignKey("ReleaseUpdateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ReleaseUpdate"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b => + { + b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null) + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.WorkspaceInvite", b => + { + b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null) + .WithMany() + .HasForeignKey("WorkspaceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b => + { + b.Navigation("ActivityEntries"); + + b.Navigation("Comments"); + + b.Navigation("Screenshot"); + + b.Navigation("Tags"); + }); + + modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdate", b => + { + b.Navigation("ReadReceipts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/Socialize.Api/Migrations/20260509170908_AddChannelImages.cs b/backend/src/Socialize.Api/Migrations/20260509170908_AddChannelImages.cs new file mode 100644 index 00000000..c5cae09f --- /dev/null +++ b/backend/src/Socialize.Api/Migrations/20260509170908_AddChannelImages.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Socialize.Api.Migrations +{ + /// + internal partial class AddChannelImages : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.AddColumn( + name: "BannerUrl", + table: "Channels", + type: "character varying(2048)", + maxLength: 2048, + nullable: true); + + migrationBuilder.AddColumn( + name: "PortraitUrl", + table: "Channels", + type: "character varying(2048)", + maxLength: 2048, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + ArgumentNullException.ThrowIfNull(migrationBuilder); + + migrationBuilder.DropColumn( + name: "BannerUrl", + table: "Channels"); + + migrationBuilder.DropColumn( + name: "PortraitUrl", + table: "Channels"); + } + } +} diff --git a/backend/src/Socialize.Api/Migrations/AppDbContextModelSnapshot.cs b/backend/src/Socialize.Api/Migrations/AppDbContextModelSnapshot.cs index 7cb97aa5..0e76e0b8 100644 --- a/backend/src/Socialize.Api/Migrations/AppDbContextModelSnapshot.cs +++ b/backend/src/Socialize.Api/Migrations/AppDbContextModelSnapshot.cs @@ -817,6 +817,10 @@ namespace Socialize.Api.Migrations .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("BannerUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + b.Property("CreatedAt") .ValueGeneratedOnAdd() .HasColumnType("timestamp with time zone") @@ -840,6 +844,10 @@ namespace Socialize.Api.Migrations .HasMaxLength(64) .HasColumnType("character varying(64)"); + b.Property("PortraitUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + b.Property("WorkspaceId") .HasColumnType("uuid"); diff --git a/backend/src/Socialize.Api/Modules/Channels/Data/Channel.cs b/backend/src/Socialize.Api/Modules/Channels/Data/Channel.cs index fd8af2ae..a3e0d6a9 100644 --- a/backend/src/Socialize.Api/Modules/Channels/Data/Channel.cs +++ b/backend/src/Socialize.Api/Modules/Channels/Data/Channel.cs @@ -8,5 +8,7 @@ internal class Channel public required string Network { get; set; } public string? Handle { get; set; } public string? ExternalUrl { get; set; } + public string? PortraitUrl { get; set; } + public string? BannerUrl { get; set; } public DateTimeOffset CreatedAt { get; init; } } diff --git a/backend/src/Socialize.Api/Modules/Channels/Data/ChannelModelConfiguration.cs b/backend/src/Socialize.Api/Modules/Channels/Data/ChannelModelConfiguration.cs index 05f55275..6b959c48 100644 --- a/backend/src/Socialize.Api/Modules/Channels/Data/ChannelModelConfiguration.cs +++ b/backend/src/Socialize.Api/Modules/Channels/Data/ChannelModelConfiguration.cs @@ -15,6 +15,8 @@ internal static class ChannelModelConfiguration 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.PortraitUrl).HasMaxLength(2048); + channel.Property(x => x.BannerUrl).HasMaxLength(2048); channel.Property(x => x.CreatedAt) .ValueGeneratedOnAdd() .HasDefaultValueSql("CURRENT_TIMESTAMP"); diff --git a/backend/src/Socialize.Api/Modules/Channels/Handlers/ChannelDtos.cs b/backend/src/Socialize.Api/Modules/Channels/Handlers/ChannelDtos.cs index 76d0a215..29445ce7 100644 --- a/backend/src/Socialize.Api/Modules/Channels/Handlers/ChannelDtos.cs +++ b/backend/src/Socialize.Api/Modules/Channels/Handlers/ChannelDtos.cs @@ -7,4 +7,6 @@ internal record ChannelDto( string Network, string? Handle, string? ExternalUrl, + string? PortraitUrl, + string? BannerUrl, DateTimeOffset CreatedAt); diff --git a/backend/src/Socialize.Api/Modules/Channels/Handlers/CreateChannel.cs b/backend/src/Socialize.Api/Modules/Channels/Handlers/CreateChannel.cs index 6e6ee264..f3c3f40e 100644 --- a/backend/src/Socialize.Api/Modules/Channels/Handlers/CreateChannel.cs +++ b/backend/src/Socialize.Api/Modules/Channels/Handlers/CreateChannel.cs @@ -11,7 +11,9 @@ internal record CreateChannelRequest( string Name, string Network, string? Handle, - string? ExternalUrl); + string? ExternalUrl, + string? PortraitUrl, + string? BannerUrl); internal class CreateChannelRequestValidator : Validator @@ -36,6 +38,8 @@ internal class CreateChannelRequestValidator .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); } } @@ -72,6 +76,8 @@ internal class CreateChannelHandler( 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( @@ -95,6 +101,8 @@ internal class CreateChannelHandler( Network = normalizedNetwork, Handle = string.IsNullOrWhiteSpace(normalizedHandle) ? null : normalizedHandle, ExternalUrl = string.IsNullOrWhiteSpace(normalizedExternalUrl) ? null : normalizedExternalUrl, + PortraitUrl = string.IsNullOrWhiteSpace(normalizedPortraitUrl) ? null : normalizedPortraitUrl, + BannerUrl = string.IsNullOrWhiteSpace(normalizedBannerUrl) ? null : normalizedBannerUrl, CreatedAt = DateTimeOffset.UtcNow, }; @@ -108,6 +116,8 @@ internal class CreateChannelHandler( channel.Network, channel.Handle, channel.ExternalUrl, + channel.PortraitUrl, + channel.BannerUrl, channel.CreatedAt); await SendAsync(dto, StatusCodes.Status201Created, ct); diff --git a/backend/src/Socialize.Api/Modules/Channels/Handlers/GetChannels.cs b/backend/src/Socialize.Api/Modules/Channels/Handlers/GetChannels.cs index c601bdc7..4e38edeb 100644 --- a/backend/src/Socialize.Api/Modules/Channels/Handlers/GetChannels.cs +++ b/backend/src/Socialize.Api/Modules/Channels/Handlers/GetChannels.cs @@ -41,6 +41,8 @@ internal class GetChannelsHandler( channel.Network, channel.Handle, channel.ExternalUrl, + channel.PortraitUrl, + channel.BannerUrl, channel.CreatedAt)) .ToListAsync(ct); diff --git a/backend/src/Socialize.Api/Modules/Channels/Handlers/UpdateChannel.cs b/backend/src/Socialize.Api/Modules/Channels/Handlers/UpdateChannel.cs new file mode 100644 index 00000000..0746cbd8 --- /dev/null +++ b/backend/src/Socialize.Api/Modules/Channels/Handlers/UpdateChannel.cs @@ -0,0 +1,120 @@ +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); + } +} diff --git a/backend/src/Socialize.Api/Modules/Channels/Handlers/UploadChannelImage.cs b/backend/src/Socialize.Api/Modules/Channels/Handlers/UploadChannelImage.cs new file mode 100644 index 00000000..ef94a929 --- /dev/null +++ b/backend/src/Socialize.Api/Modules/Channels/Handlers/UploadChannelImage.cs @@ -0,0 +1,241 @@ +using FastEndpoints; +using Microsoft.EntityFrameworkCore; +using Socialize.Api.Data; +using Socialize.Api.Infrastructure.BlobStorage.Contracts; +using Socialize.Api.Infrastructure.Security; +using Socialize.Api.Modules.Channels.Data; + +namespace Socialize.Api.Modules.Channels.Handlers; + +internal record UploadChannelImageRequest( + Guid Id, + IFormFile File); + +internal class UploadChannelImageRequestValidator + : Validator +{ + public UploadChannelImageRequestValidator() + { + RuleFor(x => x.Id).NotEmpty(); + RuleFor(x => x.File).NotNull(); + } +} + +internal class UploadChannelPortraitHandler( + AppDbContext dbContext, + AccessScopeService accessScopeService, + IBlobStorage blobStorage) + : Endpoint +{ + public override void Configure() + { + Post("/api/channels/{id}/portrait"); + Options(o => o.WithTags("Channels")); + AllowFileUploads(); + } + + public override async Task HandleAsync(UploadChannelImageRequest request, CancellationToken ct) + { + await UploadImageAsync(request, "portrait", (channel, url) => channel.PortraitUrl = url, ct); + } + + private async Task UploadImageAsync( + UploadChannelImageRequest request, + string imageKind, + Action applyImageUrl, + CancellationToken ct) + { + Channel? channel = await dbContext.Channels.SingleOrDefaultAsync(candidate => candidate.Id == request.Id, ct); + if (channel is null) + { + await SendNotFoundAsync(ct); + return; + } + + if (!await accessScopeService.CanManageWorkspaceAsync(User, channel.WorkspaceId, ct)) + { + await SendForbiddenAsync(ct); + return; + } + + string? blobUrl = await TryUploadImageAsync(request, channel, imageKind, ct); + if (blobUrl is null) + { + return; + } + + applyImageUrl(channel, blobUrl); + await dbContext.SaveChangesAsync(ct); + + await SendOkAsync(ChannelImageUploadHelpers.ToDto(channel), ct); + } + + private async Task TryUploadImageAsync( + UploadChannelImageRequest request, + Channel channel, + string imageKind, + CancellationToken ct) + { + string normalizedContentType = request.File.ContentType.Trim(); + if (request.File.Length <= 0) + { + AddError(request => request.File, "The image must not be empty."); + await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); + return null; + } + + if (!ChannelImageUploadHelpers.IsSupportedImageContentType(normalizedContentType)) + { + AddError(request => request.File, "The image must be a PNG, JPEG, GIF, or WebP file."); + await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); + return null; + } + + string fileName = ChannelImageUploadHelpers.NormalizeFileName(request.File.FileName, normalizedContentType); + string blobName = $"{channel.WorkspaceId}/{SubDirectoryNames.Profile}/channels/{channel.Id}/{imageKind}/{fileName}"; + + try + { + return await blobStorage.UploadFileAsync( + ContainerNames.Workspaces, + blobName, + request.File.OpenReadStream(), + normalizedContentType, + ct); + } + catch (InvalidOperationException) + { + AddError(request => request.File, "The image file is invalid or unsupported."); + await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); + return null; + } + } +} + +internal class UploadChannelBannerHandler( + AppDbContext dbContext, + AccessScopeService accessScopeService, + IBlobStorage blobStorage) + : Endpoint +{ + public override void Configure() + { + Post("/api/channels/{id}/banner"); + Options(o => o.WithTags("Channels")); + AllowFileUploads(); + } + + public override async Task HandleAsync(UploadChannelImageRequest request, CancellationToken ct) + { + Channel? channel = await dbContext.Channels.SingleOrDefaultAsync(candidate => candidate.Id == request.Id, ct); + if (channel is null) + { + await SendNotFoundAsync(ct); + return; + } + + if (!await accessScopeService.CanManageWorkspaceAsync(User, channel.WorkspaceId, ct)) + { + await SendForbiddenAsync(ct); + return; + } + + string normalizedContentType = request.File.ContentType.Trim(); + if (request.File.Length <= 0) + { + AddError(request => request.File, "The image must not be empty."); + await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); + return; + } + + if (!ChannelImageUploadHelpers.IsSupportedImageContentType(normalizedContentType)) + { + AddError(request => request.File, "The image must be a PNG, JPEG, GIF, or WebP file."); + await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); + return; + } + + string fileName = ChannelImageUploadHelpers.NormalizeFileName(request.File.FileName, normalizedContentType); + string blobName = $"{channel.WorkspaceId}/{SubDirectoryNames.Profile}/channels/{channel.Id}/banner/{fileName}"; + string blobUrl; + + try + { + blobUrl = await blobStorage.UploadFileAsync( + ContainerNames.Workspaces, + blobName, + request.File.OpenReadStream(), + normalizedContentType, + ct); + } + catch (InvalidOperationException) + { + AddError(request => request.File, "The image file is invalid or unsupported."); + await SendErrorsAsync(StatusCodes.Status400BadRequest, ct); + return; + } + + channel.BannerUrl = blobUrl; + await dbContext.SaveChangesAsync(ct); + + await SendOkAsync(ChannelImageUploadHelpers.ToDto(channel), ct); + } +} + +internal static class ChannelImageUploadHelpers +{ + public static ChannelDto ToDto(Channel channel) + { + return new ChannelDto( + channel.Id, + channel.WorkspaceId, + channel.Name, + channel.Network, + channel.Handle, + channel.ExternalUrl, + channel.PortraitUrl, + channel.BannerUrl, + channel.CreatedAt); + } + + public static bool IsSupportedImageContentType(string contentType) + { + string normalized = contentType.Trim(); + return normalized.Equals("image/png", StringComparison.OrdinalIgnoreCase) || + normalized.Equals("image/jpeg", StringComparison.OrdinalIgnoreCase) || + normalized.Equals("image/jpg", StringComparison.OrdinalIgnoreCase) || + normalized.Equals("image/gif", StringComparison.OrdinalIgnoreCase) || + normalized.Equals("image/webp", StringComparison.OrdinalIgnoreCase); + } + + public static string NormalizeFileName(string? fileName, string contentType) + { + string normalized = Path.GetFileName(fileName ?? string.Empty).Trim(); + if (string.IsNullOrWhiteSpace(normalized)) + { + normalized = $"channel-image{DefaultExtension(contentType)}"; + } + + return normalized.Length > 256 ? normalized[..256] : normalized; + } + + private static string DefaultExtension(string contentType) + { + if (contentType.Equals("image/png", StringComparison.OrdinalIgnoreCase)) + { + return ".png"; + } + + if (contentType.Equals("image/gif", StringComparison.OrdinalIgnoreCase)) + { + return ".gif"; + } + + if (contentType.Equals("image/webp", StringComparison.OrdinalIgnoreCase)) + { + return ".webp"; + } + + return ".jpg"; + } +} diff --git a/docs/FEATURES/channels.md b/docs/FEATURES/channels.md index ae7bdb2a..9d67d140 100644 --- a/docs/FEATURES/channels.md +++ b/docs/FEATURES/channels.md @@ -14,6 +14,8 @@ A channel has: - `network` - optional `handle` - optional `externalUrl` +- optional `portraitUrl` +- optional `bannerUrl` - `createdAt` `network` is a controlled string matching the frontend channel network options: @@ -33,6 +35,7 @@ Channel names must be unique inside a workspace for the same network. - Authenticated users with workspace access can list channels for their active workspace. - Workspace managers can create channels. +- Workspace managers can edit channel profile fields and upload portrait/banner images. - Content planning uses configured channels as selectable destinations. - Development seed data should create real workspace channels instead of relying on content target labels as fake channels. diff --git a/docs/TASKS/content/009-channel-setup-preview.md b/docs/TASKS/content/009-channel-setup-preview.md index c558e4cd..4314bc11 100644 --- a/docs/TASKS/content/009-channel-setup-preview.md +++ b/docs/TASKS/content/009-channel-setup-preview.md @@ -15,6 +15,8 @@ Make channel setup feel like configuring real social destinations and make conte - Show configured channels as compact, network-shaped preview pages. - Show a banner/profile preview while creating a channel. - Use existing channel fields: name, network, handle, and external URL. +- Let managers edit existing channel fields. +- Let managers upload portrait and banner images for channels. - In the content creation editor, show a channel setup invitation when the workspace has no configured channels. ## Validation @@ -22,6 +24,7 @@ Make channel setup feel like configuring real social destinations and make conte ```bash cd frontend npm run build +dotnet build backend/Socialize.slnx ``` ## Acceptance Criteria @@ -30,3 +33,5 @@ npm run build - [x] The channels page content area shows preview cards for configured channels. - [x] The create-channel form includes a live network-style banner/profile preview. - [x] Content creation invites the user to configure channels first when none exist. +- [x] Existing channels can be edited after creation. +- [x] Channel portrait and banner images can be uploaded and shown in previews. diff --git a/frontend/src/api/schema.d.ts b/frontend/src/api/schema.d.ts index d063723b..20898c20 100644 --- a/frontend/src/api/schema.d.ts +++ b/frontend/src/api/schema.d.ts @@ -1156,6 +1156,54 @@ export interface paths { patch?: never; trace?: never; }; + "/api/channels/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put: operations["SocializeApiModulesChannelsHandlersUpdateChannelHandler"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/channels/{id}/portrait": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["SocializeApiModulesChannelsHandlersUploadChannelPortraitHandler"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/channels/{id}/banner": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["SocializeApiModulesChannelsHandlersUploadChannelBannerHandler"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/campaigns": { parameters: { query?: never; @@ -2129,6 +2177,8 @@ export interface components { network?: string; handle?: string | null; externalUrl?: string | null; + portraitUrl?: string | null; + bannerUrl?: string | null; /** Format: date-time */ createdAt?: string; }; @@ -2139,8 +2189,24 @@ export interface components { network: string; handle?: string | null; externalUrl?: string | null; + portraitUrl?: string | null; + bannerUrl?: string | null; }; SocializeApiModulesChannelsHandlersGetChannelsRequest: Record; + SocializeApiModulesChannelsHandlersUpdateChannelRequest: { + /** Format: guid */ + workspaceId: string; + name: string; + network: string; + handle?: string | null; + externalUrl?: string | null; + portraitUrl?: string | null; + bannerUrl?: string | null; + }; + SocializeApiModulesChannelsHandlersUploadChannelImageRequest: { + /** Format: binary */ + file: string; + }; SocializeApiModulesCampaignsHandlersCampaignDto: { /** Format: guid */ id?: string; @@ -5269,6 +5335,132 @@ export interface operations { }; }; }; + SocializeApiModulesChannelsHandlersUpdateChannelHandler: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SocializeApiModulesChannelsHandlersUpdateChannelRequest"]; + }; + }; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SocializeApiModulesChannelsHandlersChannelDto"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["FastEndpointsErrorResponse"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + SocializeApiModulesChannelsHandlersUploadChannelPortraitHandler: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["SocializeApiModulesChannelsHandlersUploadChannelImageRequest"]; + }; + }; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SocializeApiModulesChannelsHandlersChannelDto"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["FastEndpointsErrorResponse"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + SocializeApiModulesChannelsHandlersUploadChannelBannerHandler: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": components["schemas"]["SocializeApiModulesChannelsHandlersUploadChannelImageRequest"]; + }; + }; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SocializeApiModulesChannelsHandlersChannelDto"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["FastEndpointsErrorResponse"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; SocializeApiModulesCampaignsHandlersGetCampaignsHandler: { parameters: { query?: { diff --git a/frontend/src/features/channels/stores/channelsStore.js b/frontend/src/features/channels/stores/channelsStore.js index e6b04fed..47c2ea6e 100644 --- a/frontend/src/features/channels/stores/channelsStore.js +++ b/frontend/src/features/channels/stores/channelsStore.js @@ -12,6 +12,7 @@ export const useChannelsStore = defineStore('channels', () => { const channels = ref([]); const isLoading = ref(false); const isCreating = ref(false); + const isUpdating = ref(false); const error = ref(null); const loadedWorkspaceId = ref(null); const allWorkspacesKey = '__all__'; @@ -104,6 +105,71 @@ export const useChannelsStore = defineStore('channels', () => { } } + async function updateChannel(channelId, payload) { + if (!authStore.isAuthenticated) { + throw new Error('You must be signed in to update a channel.'); + } + + if (isUpdating.value) { + throw new Error('A channel update request is already in progress.'); + } + + isUpdating.value = true; + error.value = null; + + try { + const response = await client.put(`/api/channels/${channelId}`, payload); + if (response.data) { + channels.value = channels.value + .map(channel => channel.id === response.data.id ? response.data : channel) + .sort((left, right) => left.name.localeCompare(right.name)); + } + + return response.data; + } catch (updateError) { + console.error('Failed to update channel:', updateError); + const message = updateError.response?.data?.errors?.[0]?.reason + ?? updateError.response?.data?.message + ?? 'Failed to update channel.'; + error.value = message; + throw new Error(message); + } finally { + isUpdating.value = false; + } + } + + async function uploadChannelImage(channelId, imageKind, file) { + if (!file) { + return null; + } + + isUpdating.value = true; + error.value = null; + + try { + const formData = new FormData(); + formData.append('file', file, file.name || `${imageKind}.jpg`); + + const response = await client.post(`/api/channels/${channelId}/${imageKind}`, formData); + if (response.data) { + channels.value = channels.value + .map(channel => channel.id === response.data.id ? response.data : channel) + .sort((left, right) => left.name.localeCompare(right.name)); + } + + return response.data; + } catch (uploadError) { + console.error('Failed to upload channel image:', uploadError); + const message = uploadError.response?.data?.errors?.[0]?.reason + ?? uploadError.response?.data?.message + ?? 'Failed to upload channel image.'; + error.value = message; + throw new Error(message); + } finally { + isUpdating.value = false; + } + } + watch( () => [authStore.isAuthenticated, workspaceStore.workspaceScopeKey], async ([isAuthenticated]) => { @@ -124,8 +190,11 @@ export const useChannelsStore = defineStore('channels', () => { channels, isLoading, isCreating, + isUpdating, error, fetchChannels, createChannel, + updateChannel, + uploadChannelImage, }; }); diff --git a/frontend/src/features/channels/views/ChannelsView.vue b/frontend/src/features/channels/views/ChannelsView.vue index 57b0f507..d71c367d 100644 --- a/frontend/src/features/channels/views/ChannelsView.vue +++ b/frontend/src/features/channels/views/ChannelsView.vue @@ -8,12 +8,15 @@ import { mdiClose, mdiFacebook, + mdiImage, mdiInstagram, mdiLinkedin, mdiMusicNote, mdiOpenInNew, + mdiPencil, mdiPlus, mdiReddit, + mdiContentSave, mdiWeb, mdiYoutube, } from '@mdi/js'; @@ -25,6 +28,7 @@ const channelsStore = useChannelsStore(); const isCreateFormVisible = ref(false); + const editingChannelId = ref(''); const formError = ref(null); const activeNetwork = ref('Instagram'); const form = reactive({ @@ -32,6 +36,8 @@ network: 'Instagram', handle: '', externalUrl: '', + portraitUrl: '', + bannerUrl: '', }); const networkOptions = [ @@ -63,11 +69,16 @@ const channelsForActiveNetwork = computed(() => configuredChannels.value.filter(channel => channel.network === activeNetwork.value) ); + const editingChannel = computed(() => + configuredChannels.value.find(channel => channel.id === editingChannelId.value) ?? null + ); const previewChannel = computed(() => ({ name: form.name.trim() || `${form.network} channel`, network: form.network, handle: form.handle.trim(), externalUrl: form.externalUrl.trim(), + portraitUrl: form.portraitUrl.trim(), + bannerUrl: form.bannerUrl.trim(), workspaceName: workspaceStore.activeWorkspace?.name ?? t('nav.noWorkspace'), scheduled: 0, readyCount: 0, @@ -95,6 +106,8 @@ form.network = activeNetwork.value; form.handle = ''; form.externalUrl = ''; + form.portraitUrl = ''; + form.bannerUrl = ''; formError.value = null; } @@ -102,6 +115,20 @@ activeNetwork.value = network; resetForm(); form.network = network; + editingChannelId.value = ''; + isCreateFormVisible.value = true; + } + + function openEditForm(channel) { + activeNetwork.value = channel.network; + editingChannelId.value = channel.id; + form.name = channel.name ?? ''; + form.network = channel.network ?? activeNetwork.value; + form.handle = channel.handle ?? ''; + form.externalUrl = channel.externalUrl ?? ''; + form.portraitUrl = channel.portraitUrl ?? ''; + form.bannerUrl = channel.bannerUrl ?? ''; + formError.value = null; isCreateFormVisible.value = true; } @@ -117,14 +144,28 @@ formError.value = null; try { - await channelsStore.createChannel({ + const payload = { name: form.name, network: form.network, handle: form.handle, externalUrl: form.externalUrl, - }); + portraitUrl: form.portraitUrl, + bannerUrl: form.bannerUrl, + }; + + if (editingChannelId.value) { + await channelsStore.updateChannel(editingChannelId.value, { + id: editingChannelId.value, + workspaceId: editingChannel.value?.workspaceId ?? workspaceStore.activeWorkspaceId, + ...payload, + }); + } else { + await channelsStore.createChannel(payload); + } + activeNetwork.value = form.network; isCreateFormVisible.value = false; + editingChannelId.value = ''; resetForm(); } catch (error) { formError.value = error.message ?? t('channels.errors.createFailed'); @@ -164,6 +205,27 @@ return `network-${(network ?? 'other').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`; } + async function uploadChannelImage(channel, imageKind, event) { + const [file] = Array.from(event.target.files ?? []); + event.target.value = ''; + + if (!file || !channel) { + return; + } + + formError.value = null; + + try { + const updated = await channelsStore.uploadChannelImage(channel.id, imageKind, file); + if (editingChannelId.value === channel.id && updated) { + form.portraitUrl = updated.portraitUrl ?? ''; + form.bannerUrl = updated.bannerUrl ?? ''; + } + } catch (error) { + formError.value = error.message ?? t('channels.errors.updateFailed'); + } + } + watch( () => route.query.create, createValue => { @@ -221,11 +283,25 @@ :class="networkClass(form.network)" >
- + +
- {{ channelInitials(previewChannel) }} + + {{ channelInitials(previewChannel) }}
{{ previewChannel.name }} @@ -240,7 +316,7 @@ @submit.prevent="submitForm" >
- {{ t('channels.createTitle') }} + {{ editingChannelId ? t('channels.editTitle') : t('channels.createTitle') }} {{ form.network }}
@@ -277,6 +353,44 @@ variant="outlined" hide-details /> + + +
+ +
+ +
@@ -290,9 +404,12 @@ - {{ channelsStore.isCreating ? t('common.saving') : t('channels.createTitle') }} + + + {{ channelsStore.isCreating || channelsStore.isUpdating ? t('common.saving') : (editingChannelId ? t('channels.saveChanges') : t('channels.createTitle')) }} +
@@ -323,7 +440,24 @@ :class="networkClass(channel.network)" >
- + +