feat: pivot to social media workflow app
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
public static class CommonFileNames
|
||||
{
|
||||
public const string ProfilePicture = "profilePicture";
|
||||
public const string LogoPicture = "logoPicture";
|
||||
public const string BannerPicture = "bannerPicture";
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
internal static class ContainerNames
|
||||
{
|
||||
public const string Users = "users";
|
||||
public const string Clients = "clients";
|
||||
public const string Creators = "creators";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
public static class ContentTypes
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
public interface IBlobStorage
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
public static class SubDirectoryNames
|
||||
{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Azure;
|
||||
using Azure.Storage.Blobs;
|
||||
using Azure.Storage.Blobs.Models;
|
||||
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
using Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
|
||||
namespace Hutopy.Infrastructure.BlobStorage.Services;
|
||||
namespace Socialize.Infrastructure.BlobStorage.Services;
|
||||
|
||||
public class AzureBlobStorage : IBlobStorage
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
namespace Hutopy.Infrastructure.Configuration;
|
||||
namespace Socialize.Infrastructure.Configuration;
|
||||
|
||||
public class WebsiteOptions
|
||||
{
|
||||
public const string SectionName = "Website";
|
||||
|
||||
public string FrontendBaseUrl { get; set; } = "https://localhost:5173";
|
||||
public string FrontendBaseUrl { get; set; } = "http://localhost:5173";
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
using Hutopy.Infrastructure.BlobStorage.Contracts;
|
||||
using Hutopy.Infrastructure.BlobStorage.Services;
|
||||
using Hutopy.Infrastructure.Configuration;
|
||||
using Hutopy.Infrastructure.Emailer.Configuration;
|
||||
using Hutopy.Infrastructure.Emailer.Contracts;
|
||||
using Hutopy.Infrastructure.Emailer.Services;
|
||||
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||
using Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||
using Hutopy.Modules.Memberships.Contracts;
|
||||
using Hutopy.Modules.Tipping.Contracts;
|
||||
using Socialize.Infrastructure.BlobStorage.Contracts;
|
||||
using Socialize.Infrastructure.BlobStorage.Services;
|
||||
using Socialize.Infrastructure.Configuration;
|
||||
using Socialize.Infrastructure.Emailer.Configuration;
|
||||
using Socialize.Infrastructure.Emailer.Contracts;
|
||||
using Socialize.Infrastructure.Emailer.Services;
|
||||
using Socialize.Infrastructure.Payments.Stripe.Configuration;
|
||||
|
||||
namespace Hutopy.Infrastructure;
|
||||
namespace Socialize.Infrastructure;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
@@ -20,11 +17,6 @@ public static class DependencyInjection
|
||||
builder.Configuration.GetRequiredSection(WebsiteOptions.SectionName));
|
||||
|
||||
builder.Services.AddTransient<IBlobStorage, AzureBlobStorage>();
|
||||
|
||||
builder.Services.AddTransient<ITipProcessor, StripeTipProcessor>();
|
||||
builder.Services.AddTransient<IMembershipPaymentProcessor, MembershipPaymentProcessor>();
|
||||
builder.Services.AddTransient<IMembershipCancellationProcessor, MembershipCancellationProcessor>();
|
||||
builder.Services.AddTransient<IMembershipTierProcessor, MembershipTierProcessor>();
|
||||
builder.Services.Configure<StripeOptions>(
|
||||
builder.Configuration.GetSection(StripeOptions.ConfigurationSection));
|
||||
|
||||
|
||||
633
backend/Infrastructure/Development/DevelopmentSeedExtensions.cs
Normal file
633
backend/Infrastructure/Development/DevelopmentSeedExtensions.cs
Normal file
@@ -0,0 +1,633 @@
|
||||
using System.Security.Claims;
|
||||
using Socialize.Infrastructure.Security;
|
||||
using Socialize.Modules.Identity.Contracts;
|
||||
using Socialize.Modules.Identity.Data;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Socialize.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 ScopedProjectId = Guid.Parse("33333333-3333-3333-3333-333333333333");
|
||||
private static readonly Guid HiddenProjectId = 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>();
|
||||
|
||||
await RemoveLegacyDevUserAsync(userManager);
|
||||
|
||||
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.ProjectScope, ScopedProjectId.ToString()),
|
||||
]);
|
||||
|
||||
await EnsureWorkspaceDataAsync(
|
||||
manager.Id,
|
||||
clientUser.Id,
|
||||
provider.Id,
|
||||
dbContext,
|
||||
cancellationToken);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task RemoveLegacyDevUserAsync(UserManager userManager)
|
||||
{
|
||||
User? legacyUser = await userManager.FindByNameAsync("dev")
|
||||
?? await userManager.FindByEmailAsync("dev@socialize.local");
|
||||
|
||||
if (legacyUser is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await userManager.DeleteAsync(legacyUser);
|
||||
}
|
||||
|
||||
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.ProjectScope 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 UpsertProjectAsync(
|
||||
dbContext,
|
||||
ScopedProjectId,
|
||||
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 UpsertProjectAsync(
|
||||
dbContext,
|
||||
HiddenProjectId,
|
||||
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,
|
||||
ScopedProjectId,
|
||||
"Spring launch hero video",
|
||||
"Fresh seasonal menu launch across Instagram and TikTok.",
|
||||
"Instagram Reel, TikTok",
|
||||
"In client review",
|
||||
DateTimeOffset.UtcNow.AddDays(3),
|
||||
"v3",
|
||||
3,
|
||||
cancellationToken);
|
||||
await UpsertContentItemAsync(
|
||||
dbContext,
|
||||
HiddenContentItemId,
|
||||
WorkspaceId,
|
||||
HiddenClientId,
|
||||
HiddenProjectId,
|
||||
"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 UpsertProjectAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid id,
|
||||
Guid workspaceId,
|
||||
Guid clientId,
|
||||
string name,
|
||||
string status,
|
||||
DateTimeOffset startDate,
|
||||
DateTimeOffset endDate,
|
||||
string? description,
|
||||
string? notes,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Project? project = await dbContext.Projects.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
|
||||
if (project is null)
|
||||
{
|
||||
project = new Project
|
||||
{
|
||||
Id = id,
|
||||
Name = string.Empty,
|
||||
Status = string.Empty,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
dbContext.Projects.Add(project);
|
||||
}
|
||||
project.WorkspaceId = workspaceId;
|
||||
project.ClientId = clientId;
|
||||
project.Name = name;
|
||||
project.Description = description;
|
||||
project.Notes = notes;
|
||||
project.Status = status;
|
||||
project.StartDate = startDate;
|
||||
project.EndDate = endDate;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task UpsertContentItemAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid id,
|
||||
Guid workspaceId,
|
||||
Guid clientId,
|
||||
Guid projectId,
|
||||
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.ProjectId = projectId;
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Socialize.Infrastructure.Development;
|
||||
|
||||
public record DevelopmentSeedOptions
|
||||
{
|
||||
public const string SectionName = "DevelopmentSeed";
|
||||
|
||||
public bool Enabled { get; init; } = true;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hutopy.Infrastructure.Emailer.Configuration;
|
||||
namespace Socialize.Infrastructure.Emailer.Configuration;
|
||||
|
||||
public class EmailerOptions
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hutopy.Infrastructure.Emailer.Contracts;
|
||||
namespace Socialize.Infrastructure.Emailer.Contracts;
|
||||
|
||||
public interface IEmailSender
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Hutopy.Infrastructure.Emailer.Contracts;
|
||||
using Socialize.Infrastructure.Emailer.Contracts;
|
||||
|
||||
namespace Hutopy.Infrastructure.Emailer.Services;
|
||||
namespace Socialize.Infrastructure.Emailer.Services;
|
||||
|
||||
public class LoggerEmailSender(ILogger<IEmailSender> logger)
|
||||
: IEmailSender
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using Hutopy.Infrastructure.Emailer.Configuration;
|
||||
using Hutopy.Infrastructure.Emailer.Contracts;
|
||||
using Socialize.Infrastructure.Emailer.Configuration;
|
||||
using Socialize.Infrastructure.Emailer.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PostmarkDotNet;
|
||||
|
||||
namespace Hutopy.Infrastructure.Emailer.Services;
|
||||
namespace Socialize.Infrastructure.Emailer.Services;
|
||||
|
||||
public class PostmarkEmailSender : IEmailSender
|
||||
{
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Hutopy.Infrastructure.Emailer.Configuration;
|
||||
using Hutopy.Infrastructure.Emailer.Contracts;
|
||||
using Socialize.Infrastructure.Emailer.Configuration;
|
||||
using Socialize.Infrastructure.Emailer.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Hutopy.Infrastructure.Emailer.Services;
|
||||
namespace Socialize.Infrastructure.Emailer.Services;
|
||||
|
||||
public class ResendEmailSender : IEmailSender
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||
namespace Socialize.Infrastructure.Payments.Stripe.Configuration;
|
||||
|
||||
public class StripeOptions
|
||||
{
|
||||
@@ -10,5 +10,5 @@ public class StripeOptions
|
||||
|
||||
[Required] public required string WebhookSecret { get; init; }
|
||||
|
||||
[Required] [Range(0, 1)] public required decimal HutopyRate { get; init; }
|
||||
[Required] [Range(0, 1)] public required decimal SocializeRate { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using Hutopy.Modules.Memberships.Contracts;
|
||||
using Stripe;
|
||||
|
||||
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||
|
||||
public sealed class MembershipCancellationProcessor
|
||||
: IMembershipCancellationProcessor
|
||||
{
|
||||
public async Task<DateTimeOffset?> CancelAsync(
|
||||
string subscriptionId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
SubscriptionService subscriptionService = new();
|
||||
|
||||
// Stripe - Cancel Subscription immediately
|
||||
// var subscription = await subscriptionService.CancelAsync(
|
||||
// subscriptionId,
|
||||
// cancellationToken: ct);
|
||||
|
||||
// Stripe - Cancel Subscription AtPeriodEnd
|
||||
Subscription? subscription = await subscriptionService.UpdateAsync(
|
||||
subscriptionId,
|
||||
new SubscriptionUpdateOptions { CancelAtPeriodEnd = true },
|
||||
cancellationToken: ct);
|
||||
|
||||
return subscription.CancelAt ?? subscription.CanceledAt;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||
using Hutopy.Modules.Creators.Contracts;
|
||||
using Hutopy.Modules.Memberships.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Stripe;
|
||||
using Stripe.Checkout;
|
||||
|
||||
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||
|
||||
public class MembershipPaymentProcessor(
|
||||
IOptions<StripeOptions> stripeOptions)
|
||||
: IMembershipPaymentProcessor
|
||||
{
|
||||
public async Task<MembershipCheckoutSession> CreateCheckoutSessionAsync(
|
||||
Guid userId,
|
||||
CreatorReference creatorReference,
|
||||
Guid tierId,
|
||||
string priceId,
|
||||
string successUrl,
|
||||
string cancelUrl)
|
||||
{
|
||||
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
||||
|
||||
// Create Stripe customer for the user if not already created
|
||||
CustomerService customerService = new();
|
||||
Customer? customer = await customerService.CreateAsync(
|
||||
new CustomerCreateOptions
|
||||
{
|
||||
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } }
|
||||
});
|
||||
|
||||
// Create Checkout Session for the subscription
|
||||
SessionService sessionService = new();
|
||||
Session? session = await sessionService.CreateAsync(
|
||||
new SessionCreateOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
PaymentMethodTypes = ["card"],
|
||||
LineItems =
|
||||
[
|
||||
new SessionLineItemOptions { Price = priceId, Quantity = 1 }
|
||||
],
|
||||
Mode = "subscription",
|
||||
SubscriptionData = new SessionSubscriptionDataOptions
|
||||
{
|
||||
ApplicationFeePercent = stripeOptions.Value.HutopyRate,
|
||||
TransferData =
|
||||
new SessionSubscriptionDataTransferDataOptions
|
||||
{
|
||||
Destination = creatorReference.StripeAccountId
|
||||
}
|
||||
},
|
||||
SuccessUrl = successUrl, // Redirect after successful payment
|
||||
CancelUrl = cancelUrl, // Redirect after canceled payment
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "userId", userId.ToString() },
|
||||
{ "creatorId", creatorReference.Id.ToString() },
|
||||
{ "creatorName", creatorReference.Name },
|
||||
{ "tierId", tierId.ToString() }
|
||||
}
|
||||
});
|
||||
|
||||
return new MembershipCheckoutSession(
|
||||
session.Id,
|
||||
session.Url);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||
using Hutopy.Modules.Memberships.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Stripe;
|
||||
|
||||
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||
|
||||
public sealed class MembershipTierProcessor(
|
||||
IOptions<StripeOptions> stripeOptions)
|
||||
: IMembershipTierProcessor
|
||||
{
|
||||
public async Task<string> CreateAsync(
|
||||
Guid creatorId,
|
||||
Guid tierId,
|
||||
string productName,
|
||||
string currencyCode,
|
||||
decimal amount)
|
||||
{
|
||||
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
||||
|
||||
// Create the product
|
||||
ProductService productService = new();
|
||||
Product? product = await productService.CreateAsync(
|
||||
new ProductCreateOptions
|
||||
{
|
||||
Name = productName,
|
||||
Metadata = { { "creatorId", creatorId.ToString() }, { "tierId", tierId.ToString() } }
|
||||
});
|
||||
|
||||
// Create the price for the product
|
||||
PriceService priceService = new();
|
||||
await priceService.CreateAsync(
|
||||
new PriceCreateOptions
|
||||
{
|
||||
Product = product.Id,
|
||||
UnitAmountDecimal = amount * 100, // Convert amount to cents
|
||||
Currency = currencyCode,
|
||||
Recurring = new PriceRecurringOptions { Interval = "month" }
|
||||
});
|
||||
|
||||
return product.Id;
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using Hutopy.Infrastructure.Payments.Stripe.Configuration;
|
||||
using Hutopy.Modules.Creators.Contracts;
|
||||
using Hutopy.Modules.Tipping.Contracts;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Stripe;
|
||||
using Stripe.Checkout;
|
||||
|
||||
namespace Hutopy.Infrastructure.Payments.Stripe.Services;
|
||||
|
||||
internal class StripeTipProcessor(
|
||||
IOptions<StripeOptions> stripeOptions)
|
||||
: ITipProcessor
|
||||
{
|
||||
public async Task<TipCheckoutSession> CreateCheckoutSessionAsync(
|
||||
Guid tipId,
|
||||
CreatorReference creator,
|
||||
decimal amount,
|
||||
string currency,
|
||||
string message,
|
||||
Uri successUrl,
|
||||
Uri cancelUrl,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var applicationFeeAmount = Convert.ToInt64(amount * stripeOptions.Value.HutopyRate);
|
||||
|
||||
StripeConfiguration.ApiKey = stripeOptions.Value.SecretKey;
|
||||
|
||||
var sessionService = new SessionService();
|
||||
|
||||
var options = new SessionCreateOptions
|
||||
{
|
||||
ClientReferenceId = tipId.ToString(),
|
||||
Mode = "payment",
|
||||
LineItems =
|
||||
[
|
||||
new SessionLineItemOptions
|
||||
{
|
||||
PriceData = new SessionLineItemPriceDataOptions
|
||||
{
|
||||
Currency = currency,
|
||||
UnitAmountDecimal = amount, // Amount in cents
|
||||
ProductData = new SessionLineItemPriceDataProductDataOptions
|
||||
{
|
||||
Name = $"Tip for {creator.Name}",
|
||||
Metadata = new Dictionary<string, string> { { "creatorId", creator.Id.ToString() } }
|
||||
}
|
||||
},
|
||||
Quantity = 1
|
||||
}
|
||||
],
|
||||
PaymentIntentData = new SessionPaymentIntentDataOptions { ApplicationFeeAmount = applicationFeeAmount },
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "creatorId", creator.Id.ToString() }, { "creatorName", creator.Name }, { "message", message }
|
||||
},
|
||||
SuccessUrl = successUrl.ToString(), // Redirect after successful payment
|
||||
CancelUrl = cancelUrl.ToString(), // Redirect after canceled payment
|
||||
};
|
||||
|
||||
var requestOptions = new RequestOptions { StripeAccount = creator.StripeAccountId };
|
||||
|
||||
var session = await sessionService.CreateAsync(
|
||||
options,
|
||||
requestOptions,
|
||||
cancellationToken: ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return new TipCheckoutSession(session.Id, session.Url);
|
||||
}
|
||||
}
|
||||
56
backend/Infrastructure/Security/AccessScopeService.cs
Normal file
56
backend/Infrastructure/Security/AccessScopeService.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Security.Claims;
|
||||
using Socialize.Modules.Identity.Contracts;
|
||||
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
|
||||
public sealed class AccessScopeService
|
||||
{
|
||||
public bool IsManager(ClaimsPrincipal user)
|
||||
{
|
||||
return user.IsInRole(KnownRoles.Administrator) || user.IsInRole(KnownRoles.Manager);
|
||||
}
|
||||
|
||||
public bool IsProvider(ClaimsPrincipal user)
|
||||
{
|
||||
return user.IsInRole(KnownRoles.Provider);
|
||||
}
|
||||
|
||||
public bool IsClient(ClaimsPrincipal user)
|
||||
{
|
||||
return user.IsInRole(KnownRoles.Client);
|
||||
}
|
||||
|
||||
public bool CanAccessWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
||||
{
|
||||
return IsManager(user) || user.GetWorkspaceScopeIds().Contains(workspaceId);
|
||||
}
|
||||
|
||||
public bool CanManageWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
||||
{
|
||||
return IsManager(user) && CanAccessWorkspace(user, workspaceId);
|
||||
}
|
||||
|
||||
public bool CanAccessClient(ClaimsPrincipal user, Guid workspaceId, Guid clientId)
|
||||
{
|
||||
return IsManager(user)
|
||||
|| (CanAccessWorkspace(user, workspaceId) && user.GetClientScopeIds().Contains(clientId));
|
||||
}
|
||||
|
||||
public bool CanAccessProject(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
|
||||
{
|
||||
return IsManager(user)
|
||||
|| (CanAccessClient(user, workspaceId, clientId) && user.GetProjectScopeIds().Contains(projectId));
|
||||
}
|
||||
|
||||
public bool CanContributeToProject(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
|
||||
{
|
||||
return IsManager(user) || (IsProvider(user) && CanAccessProject(user, workspaceId, clientId, projectId));
|
||||
}
|
||||
|
||||
public bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
|
||||
{
|
||||
return IsManager(user)
|
||||
|| IsProvider(user) && CanAccessProject(user, workspaceId, clientId, projectId)
|
||||
|| IsClient(user) && CanAccessClient(user, workspaceId, clientId);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,38 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
|
||||
public static class ClaimsPrincipalExtensions
|
||||
{
|
||||
public static IReadOnlyCollection<Guid> GetScopeIds(this ClaimsPrincipal claims, string key)
|
||||
{
|
||||
return claims.FindAll(key)
|
||||
.Select(claim => Guid.TryParse(claim.Value, out Guid value) ? value : Guid.Empty)
|
||||
.Where(value => value != Guid.Empty)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<Guid> GetWorkspaceScopeIds(this ClaimsPrincipal claims)
|
||||
{
|
||||
return claims.GetScopeIds(KnownClaims.WorkspaceScope);
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<Guid> GetClientScopeIds(this ClaimsPrincipal claims)
|
||||
{
|
||||
return claims.GetScopeIds(KnownClaims.ClientScope);
|
||||
}
|
||||
|
||||
public static IReadOnlyCollection<Guid> GetProjectScopeIds(this ClaimsPrincipal claims)
|
||||
{
|
||||
return claims.GetScopeIds(KnownClaims.ProjectScope);
|
||||
}
|
||||
|
||||
public static string? GetPersona(this ClaimsPrincipal claims)
|
||||
{
|
||||
return (string?)claims.GetClaim<string?>(KnownClaims.Persona);
|
||||
}
|
||||
|
||||
public static Guid GetUserId(this ClaimsPrincipal claims)
|
||||
{
|
||||
return (Guid)claims.GetRequiredClaim<Guid>(ClaimTypes.NameIdentifier);
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Security.Claims;
|
||||
using System.Text;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
|
||||
public static class JwtTokenHelper
|
||||
{
|
||||
@@ -17,7 +17,9 @@ public static class JwtTokenHelper
|
||||
string? alias,
|
||||
string firstname,
|
||||
string lastname,
|
||||
string? portraitUrl)
|
||||
string? portraitUrl,
|
||||
IEnumerable<string> roles,
|
||||
IEnumerable<Claim> additionalClaims)
|
||||
{
|
||||
SymmetricSecurityKey securityKey = new(Encoding.UTF8.GetBytes(key));
|
||||
SigningCredentials credentials = new(securityKey, SecurityAlgorithms.HmacSha256);
|
||||
@@ -40,6 +42,18 @@ public static class JwtTokenHelper
|
||||
claims.Add(new Claim(KnownClaims.PortraitUrl, portraitUrl));
|
||||
}
|
||||
|
||||
foreach (string role in roles.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
claims.Add(new Claim(ClaimTypes.Role, role));
|
||||
}
|
||||
|
||||
foreach (Claim claim in additionalClaims
|
||||
.Where(claim => !string.IsNullOrWhiteSpace(claim.Type) && !string.IsNullOrWhiteSpace(claim.Value))
|
||||
.DistinctBy(claim => $"{claim.Type}:{claim.Value}"))
|
||||
{
|
||||
claims.Add(claim);
|
||||
}
|
||||
|
||||
JwtSecurityToken token = new(
|
||||
issuer,
|
||||
audience,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
|
||||
public static class KnownClaims
|
||||
{
|
||||
public const string Alias = "alias";
|
||||
public const string PortraitUrl = "portraitUrl";
|
||||
public const string WorkspaceScope = "workspace";
|
||||
public const string ClientScope = "client";
|
||||
public const string ProjectScope = "project";
|
||||
public const string Persona = "persona";
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
|
||||
public class MissingClaimException(
|
||||
string claimName)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
|
||||
// If we need to add special characters we can alternate between 2 pools.
|
||||
public static class PasswordGenerator
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Hutopy.Infrastructure.Security;
|
||||
namespace Socialize.Infrastructure.Security;
|
||||
|
||||
public static class RefreshTokenGenerator
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Hutopy.Infrastructure.YouTube;
|
||||
namespace Socialize.Infrastructure.YouTube;
|
||||
|
||||
public static class YouTubeUrlHelper
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user