Files
social-media/backend/src/Socialize.Api/Infrastructure/Development/DevelopmentSeedExtensions.cs

644 lines
27 KiB
C#

using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Identity.Contracts;
using Socialize.Api.Modules.Identity.Data;
using Socialize.Api.Modules.Assets.Data;
using Socialize.Api.Modules.Approvals.Data;
using Socialize.Api.Modules.Comments.Data;
using Socialize.Api.Modules.ContentItems.Data;
using Socialize.Api.Modules.Clients.Data;
using Socialize.Api.Modules.Notifications.Data;
using Socialize.Api.Modules.Campaigns.Data;
using Socialize.Api.Modules.Workspaces.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace Socialize.Api.Infrastructure.Development;
public static class DevelopmentSeedExtensions
{
private static readonly Guid WorkspaceId = Guid.Parse("11111111-1111-1111-1111-111111111111");
private static readonly Guid ScopedClientId = Guid.Parse("22222222-2222-2222-2222-222222222222");
private static readonly Guid HiddenClientId = Guid.Parse("22222222-2222-2222-2222-333333333333");
private static readonly Guid ScopedCampaignId = Guid.Parse("33333333-3333-3333-3333-333333333333");
private static readonly Guid HiddenCampaignId = Guid.Parse("33333333-3333-3333-3333-444444444444");
private static readonly Guid ScopedContentItemId = Guid.Parse("44444444-4444-4444-4444-444444444444");
private static readonly Guid HiddenContentItemId = Guid.Parse("44444444-4444-4444-4444-555555555555");
private static readonly Guid ScopedAssetId = Guid.Parse("55555555-5555-5555-5555-555555555555");
private static readonly Guid ScopedApprovalRequestId = Guid.Parse("66666666-6666-6666-6666-666666666666");
private static readonly Guid ClientCommentId = Guid.Parse("77777777-7777-7777-7777-777777777777");
private static readonly Guid NotificationId = Guid.Parse("88888888-8888-8888-8888-888888888888");
public static async Task<IApplicationBuilder> UseDevelopmentSeedAsync(
this IApplicationBuilder app,
CancellationToken cancellationToken = default)
{
IHostEnvironment environment = app.ApplicationServices.GetRequiredService<IHostEnvironment>();
if (!environment.IsDevelopment())
{
return app;
}
using IServiceScope scope = app.ApplicationServices.CreateScope();
IOptions<DevelopmentSeedOptions> options = scope.ServiceProvider.GetRequiredService<IOptions<DevelopmentSeedOptions>>();
if (!options.Value.Enabled)
{
return app;
}
UserManager userManager = scope.ServiceProvider.GetRequiredService<UserManager>();
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
User manager = await EnsureUserAsync(
userManager,
id: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
username: "manager",
email: "manager@socialize.local",
password: "manager",
alias: "Northstar Manager",
firstname: "Morgan",
lastname: "Reid",
portraitUrl: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80",
roles: [KnownRoles.Administrator, KnownRoles.Manager, KnownRoles.WorkspaceMember],
claims:
[
new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()),
]);
User clientUser = await EnsureUserAsync(
userManager,
id: Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
username: "client",
email: "client@socialize.local",
password: "client",
alias: "Sofia Martin",
firstname: "Sofia",
lastname: "Martin",
portraitUrl: "https://images.unsplash.com/photo-1544723795-3fb6469f5b39?auto=format&fit=crop&w=200&q=80",
roles: [KnownRoles.Client, KnownRoles.WorkspaceMember],
claims:
[
new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()),
new Claim(KnownClaims.ClientScope, ScopedClientId.ToString()),
]);
User provider = await EnsureUserAsync(
userManager,
id: Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
username: "provider",
email: "provider@socialize.local",
password: "provider",
alias: "Alex Studio",
firstname: "Alex",
lastname: "Studio",
portraitUrl: "https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=200&q=80",
roles: [KnownRoles.Provider, KnownRoles.WorkspaceMember],
claims:
[
new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()),
new Claim(KnownClaims.ClientScope, ScopedClientId.ToString()),
new Claim(KnownClaims.CampaignScope, ScopedCampaignId.ToString()),
]);
User dev = await EnsureUserAsync(
userManager,
id: Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"),
username: "dev",
email: "dev@socialize.local",
password: "dev",
alias: "Socialize Dev",
firstname: "Jo",
lastname: "Bumble",
portraitUrl: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80",
roles: [KnownRoles.Developer, KnownRoles.Administrator, KnownRoles.Manager, KnownRoles.WorkspaceMember],
claims:
[
]);
await EnsureWorkspaceDataAsync(
manager.Id,
clientUser.Id,
provider.Id,
dbContext,
cancellationToken);
return app;
}
private static async Task<User> EnsureUserAsync(
UserManager userManager,
Guid id,
string username,
string email,
string password,
string alias,
string firstname,
string lastname,
string? portraitUrl,
IReadOnlyCollection<string> roles,
IReadOnlyCollection<Claim> claims)
{
User? user = await userManager.FindByNameAsync(username)
?? await userManager.FindByEmailAsync(email);
if (user is null)
{
user = new User
{
Id = id,
UserName = username,
Email = email,
Alias = alias,
Firstname = firstname,
Lastname = lastname,
PortraitUrl = portraitUrl,
EmailConfirmed = true,
};
IdentityResult createResult = await userManager.CreateAsync(user, password);
if (!createResult.Succeeded)
{
throw new InvalidOperationException(
$"Failed to seed development user '{username}': {string.Join(", ", createResult.Errors.Select(error => error.Description))}");
}
}
user.UserName = username;
user.Email = email;
user.Alias = alias;
user.Firstname = firstname;
user.Lastname = lastname;
user.PortraitUrl = portraitUrl;
user.EmailConfirmed = true;
await userManager.UpdateAsync(user);
if (!await userManager.CheckPasswordAsync(user, password))
{
string resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
IdentityResult passwordResetResult = await userManager.ResetPasswordAsync(user, resetToken, password);
if (!passwordResetResult.Succeeded)
{
throw new InvalidOperationException(
$"Failed to set development password for '{username}': {string.Join(", ", passwordResetResult.Errors.Select(error => error.Description))}");
}
}
IList<string> existingRoles = await userManager.GetRolesAsync(user);
foreach (string role in roles.Except(existingRoles, StringComparer.Ordinal))
{
await userManager.AddToRoleAsync(user, role);
}
foreach (string role in existingRoles
.Where(role => role is KnownRoles.Manager or KnownRoles.Client or KnownRoles.Provider or KnownRoles.Administrator or KnownRoles.WorkspaceMember)
.Except(roles, StringComparer.Ordinal))
{
await userManager.RemoveFromRoleAsync(user, role);
}
IList<Claim> existingClaims = await userManager.GetClaimsAsync(user);
List<Claim> managedClaims = existingClaims
.Where(claim => claim.Type is KnownClaims.WorkspaceScope or KnownClaims.ClientScope or KnownClaims.CampaignScope or KnownClaims.Persona)
.ToList();
foreach (Claim claim in managedClaims)
{
await userManager.RemoveClaimAsync(user, claim);
}
string persona = roles.Contains(KnownRoles.Manager, StringComparer.Ordinal)
? KnownRoles.Manager
: roles.Contains(KnownRoles.Client, StringComparer.Ordinal)
? KnownRoles.Client
: roles.Contains(KnownRoles.Provider, StringComparer.Ordinal)
? KnownRoles.Provider
: KnownRoles.WorkspaceMember;
foreach (Claim claim in claims.Concat([new Claim(KnownClaims.Persona, persona)]))
{
await userManager.AddClaimAsync(user, claim);
}
return user;
}
private static async Task EnsureWorkspaceDataAsync(
Guid managerUserId,
Guid clientUserId,
Guid providerUserId,
AppDbContext dbContext,
CancellationToken cancellationToken)
{
Workspace? workspace = await dbContext.Workspaces
.SingleOrDefaultAsync(candidate => candidate.Id == WorkspaceId, cancellationToken);
if (workspace is null)
{
workspace = new Workspace
{
Id = WorkspaceId,
Name = string.Empty,
Slug = string.Empty,
TimeZone = string.Empty,
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.Workspaces.Add(workspace);
}
workspace.Name = "Northstar Studio";
workspace.Slug = "northstar-studio";
workspace.OwnerUserId = managerUserId;
workspace.TimeZone = "America/Montreal";
await dbContext.SaveChangesAsync(cancellationToken);
await UpsertClientAsync(
dbContext,
ScopedClientId,
"Luma Coffee",
"Active",
"https://images.unsplash.com/photo-1511920170033-f8396924c348?auto=format&fit=crop&w=200&q=80",
"Sofia Martin",
"client@socialize.local",
WorkspaceId,
cancellationToken);
await UpsertClientAsync(
dbContext,
HiddenClientId,
"Atlas Bakery",
"Active",
"https://images.unsplash.com/photo-1509440159596-0249088772ff?auto=format&fit=crop&w=200&q=80",
"Nina Cole",
"nina@atlasbakery.test",
WorkspaceId,
cancellationToken);
await UpsertCampaignAsync(
dbContext,
ScopedCampaignId,
WorkspaceId,
ScopedClientId,
"Spring Launch",
"In progress",
DateTimeOffset.UtcNow.AddDays(1),
DateTimeOffset.UtcNow.AddDays(7),
"Cross-channel launch campaign for the spring offer.",
"Coordinate creative approvals before the final week.",
cancellationToken);
await UpsertCampaignAsync(
dbContext,
HiddenCampaignId,
WorkspaceId,
HiddenClientId,
"Summer Retention",
"Planned",
DateTimeOffset.UtcNow.AddDays(10),
DateTimeOffset.UtcNow.AddDays(16),
"Retention campaign aimed at existing subscribers.",
"Sequence email and paid social updates together.",
cancellationToken);
await UpsertContentItemAsync(
dbContext,
ScopedContentItemId,
WorkspaceId,
ScopedClientId,
ScopedCampaignId,
"Spring launch hero video",
"Fresh seasonal menu launch across Instagram and TikTok.",
"Instagram Reel, TikTok",
"In approval",
DateTimeOffset.UtcNow.AddDays(3),
"v3",
3,
cancellationToken);
await UpsertContentItemAsync(
dbContext,
HiddenContentItemId,
WorkspaceId,
HiddenClientId,
HiddenCampaignId,
"Bakery loyalty carousel",
"Reward regular customers with a four-card retention carousel.",
"Instagram Carousel",
"Draft",
DateTimeOffset.UtcNow.AddDays(10),
"v1",
1,
cancellationToken);
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000001"), ScopedContentItemId, 1, "v1", "Spring launch hero video", "Initial draft for the seasonal menu launch.", "Instagram Reel, TikTok", "Initial concept draft.", providerUserId, DateTimeOffset.UtcNow.AddDays(-5), cancellationToken);
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000002"), ScopedContentItemId, 2, "v2", "Spring launch hero video", "Updated hook and transitions after internal review.", "Instagram Reel, TikTok", "Addressed internal pacing feedback.", providerUserId, DateTimeOffset.UtcNow.AddDays(-3), cancellationToken);
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000003"), ScopedContentItemId, 3, "v3", "Spring launch hero video", "Fresh seasonal menu launch across Instagram and TikTok.", "Instagram Reel, TikTok", "Client-facing draft after copy cleanup.", providerUserId, DateTimeOffset.UtcNow.AddDays(-1), cancellationToken);
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000004"), HiddenContentItemId, 1, "v1", "Bakery loyalty carousel", "Reward regular customers with a four-card retention carousel.", "Instagram Carousel", "First draft.", managerUserId, DateTimeOffset.UtcNow.AddDays(-2), cancellationToken);
Asset? asset = await dbContext.Assets.SingleOrDefaultAsync(candidate => candidate.Id == ScopedAssetId, cancellationToken);
if (asset is null)
{
asset = new Asset
{
Id = ScopedAssetId,
AssetType = string.Empty,
SourceType = string.Empty,
DisplayName = string.Empty,
CreatedAt = DateTimeOffset.UtcNow.AddDays(-4),
};
dbContext.Assets.Add(asset);
}
asset.WorkspaceId = WorkspaceId;
asset.ContentItemId = ScopedContentItemId;
asset.AssetType = "Video";
asset.SourceType = "GoogleDrive";
asset.DisplayName = "Spring launch cut";
asset.GoogleDriveFileId = "dev-socialize-demo";
asset.GoogleDriveLink = "https://drive.google.com/file/d/dev-socialize-demo/view";
asset.PreviewUrl = "https://drive.google.com/thumbnail?id=dev-socialize-demo";
asset.CurrentRevisionNumber = 2;
await dbContext.SaveChangesAsync(cancellationToken);
await EnsureAssetRevisionAsync(dbContext, Guid.Parse("55555555-5555-5555-5555-000000000001"), ScopedAssetId, 1, "https://drive.google.com/file/d/dev-socialize-demo-v1/view", "https://drive.google.com/thumbnail?id=dev-socialize-demo-v1", "First uploaded cut from the editor.", providerUserId, DateTimeOffset.UtcNow.AddDays(-4), cancellationToken);
await EnsureAssetRevisionAsync(dbContext, Guid.Parse("55555555-5555-5555-5555-000000000002"), ScopedAssetId, 2, "https://drive.google.com/file/d/dev-socialize-demo-v2/view", "https://drive.google.com/thumbnail?id=dev-socialize-demo-v2", "Re-export with pacing changes and updated title card.", providerUserId, DateTimeOffset.UtcNow.AddDays(-2), cancellationToken);
Comment? comment = await dbContext.Comments.SingleOrDefaultAsync(candidate => candidate.Id == ClientCommentId, cancellationToken);
if (comment is null)
{
comment = new Comment
{
Id = ClientCommentId,
AuthorDisplayName = string.Empty,
AuthorEmail = string.Empty,
Body = string.Empty,
CreatedAt = DateTimeOffset.UtcNow.AddHours(-20),
};
dbContext.Comments.Add(comment);
}
comment.WorkspaceId = WorkspaceId;
comment.ContentItemId = ScopedContentItemId;
comment.AuthorUserId = clientUserId;
comment.AuthorDisplayName = "Sofia Martin";
comment.AuthorEmail = "client@socialize.local";
comment.Body = "Please tighten the opening three seconds and make the launch CTA more explicit.";
comment.IsResolved = false;
comment.ResolvedAt = null;
await dbContext.SaveChangesAsync(cancellationToken);
ApprovalRequest? approvalRequest = await dbContext.ApprovalRequests.SingleOrDefaultAsync(candidate => candidate.Id == ScopedApprovalRequestId, cancellationToken);
if (approvalRequest is null)
{
approvalRequest = new ApprovalRequest
{
Id = ScopedApprovalRequestId,
Stage = string.Empty,
ReviewerName = string.Empty,
ReviewerEmail = string.Empty,
State = string.Empty,
AccessToken = string.Empty,
SentAt = DateTimeOffset.UtcNow.AddHours(-12),
};
dbContext.ApprovalRequests.Add(approvalRequest);
}
approvalRequest.WorkspaceId = WorkspaceId;
approvalRequest.ContentItemId = ScopedContentItemId;
approvalRequest.Stage = "Client";
approvalRequest.ReviewerName = "Sofia Martin";
approvalRequest.ReviewerEmail = "client@socialize.local";
approvalRequest.RequestedByUserId = managerUserId;
approvalRequest.DueAt = DateTimeOffset.UtcNow.AddDays(1);
approvalRequest.State = "Pending";
approvalRequest.AccessToken = "seed-client-review-token";
approvalRequest.CompletedAt = null;
await dbContext.SaveChangesAsync(cancellationToken);
NotificationEvent? approvalNotification = await dbContext.NotificationEvents.SingleOrDefaultAsync(candidate => candidate.Id == NotificationId, cancellationToken);
if (approvalNotification is null)
{
approvalNotification = new NotificationEvent
{
Id = NotificationId,
EventType = string.Empty,
EntityType = string.Empty,
Message = string.Empty,
CreatedAt = DateTimeOffset.UtcNow.AddHours(-12),
};
dbContext.NotificationEvents.Add(approvalNotification);
}
approvalNotification.WorkspaceId = WorkspaceId;
approvalNotification.ContentItemId = ScopedContentItemId;
approvalNotification.EventType = "approval.requested";
approvalNotification.EntityType = "ApprovalRequest";
approvalNotification.EntityId = ScopedApprovalRequestId;
approvalNotification.Message = "Approval requested from Sofia Martin for Spring launch hero video.";
approvalNotification.RecipientEmail = "client@socialize.local";
approvalNotification.MetadataJson = """{"stage":"Client"}""";
approvalNotification.ReadAt = null;
Guid commentNotificationId = Guid.Parse("88888888-8888-8888-8888-000000000002");
NotificationEvent? commentNotification = await dbContext.NotificationEvents.SingleOrDefaultAsync(candidate => candidate.Id == commentNotificationId, cancellationToken);
if (commentNotification is null)
{
commentNotification = new NotificationEvent
{
Id = commentNotificationId,
EventType = string.Empty,
EntityType = string.Empty,
Message = string.Empty,
CreatedAt = DateTimeOffset.UtcNow.AddHours(-20),
};
dbContext.NotificationEvents.Add(commentNotification);
}
commentNotification.WorkspaceId = WorkspaceId;
commentNotification.ContentItemId = ScopedContentItemId;
commentNotification.EventType = "comment.created";
commentNotification.EntityType = "Comment";
commentNotification.EntityId = ClientCommentId;
commentNotification.Message = "Sofia Martin commented on Spring launch hero video.";
commentNotification.RecipientUserId = managerUserId;
commentNotification.RecipientEmail = "manager@socialize.local";
commentNotification.MetadataJson = null;
commentNotification.ReadAt = null;
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task UpsertClientAsync(
AppDbContext dbContext,
Guid id,
string name,
string status,
string portraitUrl,
string primaryContactName,
string primaryContactEmail,
Guid workspaceId,
CancellationToken cancellationToken)
{
Client? client = await dbContext.Clients.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
if (client is null)
{
client = new Client
{
Id = id,
Name = string.Empty,
Status = string.Empty,
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.Clients.Add(client);
}
client.WorkspaceId = workspaceId;
client.Name = name;
client.Status = status;
client.PortraitUrl = portraitUrl;
client.PrimaryContactName = primaryContactName;
client.PrimaryContactEmail = primaryContactEmail;
client.PrimaryContactPortraitUrl = null;
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task UpsertCampaignAsync(
AppDbContext dbContext,
Guid id,
Guid workspaceId,
Guid clientId,
string name,
string status,
DateTimeOffset startDate,
DateTimeOffset endDate,
string? description,
string? notes,
CancellationToken cancellationToken)
{
Campaign? campaign = await dbContext.Campaigns.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
if (campaign is null)
{
campaign = new Campaign
{
Id = id,
Name = string.Empty,
Status = string.Empty,
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.Campaigns.Add(campaign);
}
campaign.WorkspaceId = workspaceId;
campaign.ClientId = clientId;
campaign.Name = name;
campaign.Description = description;
campaign.Notes = notes;
campaign.Status = status;
campaign.StartDate = startDate;
campaign.EndDate = endDate;
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task UpsertContentItemAsync(
AppDbContext dbContext,
Guid id,
Guid workspaceId,
Guid clientId,
Guid campaignId,
string title,
string publicationMessage,
string publicationTargets,
string status,
DateTimeOffset? dueDate,
string currentRevisionLabel,
int currentRevisionNumber,
CancellationToken cancellationToken)
{
ContentItem? item = await dbContext.ContentItems.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
if (item is null)
{
item = new ContentItem
{
Id = id,
Title = string.Empty,
PublicationMessage = string.Empty,
PublicationTargets = string.Empty,
Status = string.Empty,
CurrentRevisionLabel = string.Empty,
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.ContentItems.Add(item);
}
item.WorkspaceId = workspaceId;
item.ClientId = clientId;
item.CampaignId = campaignId;
item.Title = title;
item.PublicationMessage = publicationMessage;
item.PublicationTargets = publicationTargets;
item.Status = status;
item.DueDate = dueDate;
item.CurrentRevisionLabel = currentRevisionLabel;
item.CurrentRevisionNumber = currentRevisionNumber;
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task EnsureRevisionAsync(
AppDbContext dbContext,
Guid id,
Guid contentItemId,
int revisionNumber,
string revisionLabel,
string title,
string publicationMessage,
string publicationTargets,
string changeSummary,
Guid createdByUserId,
DateTimeOffset createdAt,
CancellationToken cancellationToken)
{
ContentItemRevision? revision = await dbContext.ContentItemRevisions.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
if (revision is null)
{
revision = new ContentItemRevision
{
Id = id,
RevisionLabel = string.Empty,
Title = string.Empty,
PublicationMessage = string.Empty,
PublicationTargets = string.Empty,
CreatedAt = createdAt,
};
dbContext.ContentItemRevisions.Add(revision);
}
revision.ContentItemId = contentItemId;
revision.RevisionNumber = revisionNumber;
revision.RevisionLabel = revisionLabel;
revision.Title = title;
revision.PublicationMessage = publicationMessage;
revision.PublicationTargets = publicationTargets;
revision.ChangeSummary = changeSummary;
revision.CreatedByUserId = createdByUserId;
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task EnsureAssetRevisionAsync(
AppDbContext dbContext,
Guid id,
Guid assetId,
int revisionNumber,
string sourceReference,
string? previewUrl,
string? notes,
Guid createdByUserId,
DateTimeOffset createdAt,
CancellationToken cancellationToken)
{
AssetRevision? revision = await dbContext.AssetRevisions.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
if (revision is null)
{
revision = new AssetRevision
{
Id = id,
SourceReference = string.Empty,
CreatedAt = createdAt,
};
dbContext.AssetRevisions.Add(revision);
}
revision.AssetId = assetId;
revision.RevisionNumber = revisionNumber;
revision.SourceReference = sourceReference;
revision.PreviewUrl = previewUrl;
revision.Notes = notes;
revision.CreatedByUserId = createdByUserId;
await dbContext.SaveChangesAsync(cancellationToken);
}
}