Compare commits

12 Commits

Author SHA1 Message Date
664eb07201 Polish workspace organization selector
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled
2026-05-04 17:44:39 -04:00
58c1301054 refactor: remove organization slug 2026-05-04 17:41:50 -04:00
552f4f1f21 fix: collapse sidebar by default on small screens 2026-05-04 16:40:43 -04:00
8f4b95f311 feat: add organization settings UI 2026-05-04 16:33:34 -04:00
4fba72e99c feat: prerender public site pages 2026-05-04 16:29:50 -04:00
55d8acef4c Refine content approval workflow rail 2026-05-04 16:20:32 -04:00
7d3f495472 feat: add organization domain foundation 2026-05-04 16:15:53 -04:00
802668fb0b feat: add public site pages and social login 2026-05-04 16:13:57 -04:00
cd6f402d9e docs: define organization account model 2026-05-04 15:45:12 -04:00
9bdef978bd refactor: align main layout shell 2026-05-04 14:46:13 -04:00
2d472892d6 Merge branch 'approval-workflow-docs' into HEAD
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled
# Conflicts:
#	frontend/src/api/schema.d.ts
#	frontend/src/features/content/views/ContentItemDetailView.vue
#	frontend/src/features/workspaces/views/DashboardView.vue
#	shared/openapi/openapi.json
2026-05-01 15:58:04 -04:00
884ca4b96d chore: add missing multi-level editor for approval workflow, rename projects to campaings. 2026-05-01 15:50:02 -04:00
188 changed files with 9773 additions and 2122 deletions

View File

@@ -70,6 +70,7 @@ Update OpenAPI:
## Current Domain Modules
- `Identity`: authentication, refresh tokens, email verification, password reset, social login.
- `Organizations`: SaaS account ownership, billing/subscription boundary, organization membership, connectors, data mappings, and owned workspaces.
- `Workspaces`: workspace membership, workspace settings, access scoping.
- `Clients`: client records and primary contacts tied to workspaces.
- `Projects`: project pipeline and client/project relationships.

View File

@@ -1,6 +1,6 @@
# Socialize
Socialize is a workspace-based workflow application for social media content review, revision, approval, and publication readiness.
Socialize is an organization-owned, workspace-based workflow application for social media content review, revision, approval, and publication readiness.
It is not a public social network. The product is for internal teams, providers, and client approvers coordinating content work before publication.

View File

@@ -8,7 +8,8 @@ using Socialize.Api.Modules.ContentItems.Data;
using Socialize.Api.Modules.Feedback.Data;
using Socialize.Api.Modules.Identity.Data;
using Socialize.Api.Modules.Notifications.Data;
using Socialize.Api.Modules.Projects.Data;
using Socialize.Api.Modules.Campaigns.Data;
using Socialize.Api.Modules.Organizations.Data;
using Socialize.Api.Modules.Workspaces.Data;
namespace Socialize.Api.Data;
@@ -17,10 +18,12 @@ public class AppDbContext(
DbContextOptions<AppDbContext> options)
: IdentityDbContext<User, Role, Guid>(options)
{
public DbSet<Organization> Organizations => Set<Organization>();
public DbSet<OrganizationMembership> OrganizationMemberships => Set<OrganizationMembership>();
public DbSet<Workspace> Workspaces => Set<Workspace>();
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
public DbSet<Client> Clients => Set<Client>();
public DbSet<Project> Projects => Set<Project>();
public DbSet<Campaign> Campaigns => Set<Campaign>();
public DbSet<ContentItem> ContentItems => Set<ContentItem>();
public DbSet<ContentItemRevision> ContentItemRevisions => Set<ContentItemRevision>();
public DbSet<Asset> Assets => Set<Asset>();
@@ -41,9 +44,10 @@ public class AppDbContext(
{
base.OnModelCreating(builder);
builder.ConfigureOrganizationsModule();
builder.ConfigureWorkspacesModule();
builder.ConfigureClientsModule();
builder.ConfigureProjectsModule();
builder.ConfigureCampaignsModule();
builder.ConfigureContentItemsModule();
builder.ConfigureAssetsModule();
builder.ConfigureCommentsModule();

View File

@@ -10,7 +10,9 @@ 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.Projects.Data;
using Socialize.Api.Modules.Campaigns.Data;
using Socialize.Api.Modules.Organizations.Data;
using Socialize.Api.Modules.Organizations.Services;
using Socialize.Api.Modules.Workspaces.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
@@ -19,11 +21,12 @@ namespace Socialize.Api.Infrastructure.Development;
public static class DevelopmentSeedExtensions
{
private static readonly Guid OrganizationId = Guid.Parse("99999999-9999-9999-9999-999999999999");
private static readonly Guid WorkspaceId = Guid.Parse("11111111-1111-1111-1111-111111111111");
private static readonly Guid 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 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");
@@ -99,7 +102,7 @@ public static class DevelopmentSeedExtensions
[
new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()),
new Claim(KnownClaims.ClientScope, ScopedClientId.ToString()),
new Claim(KnownClaims.ProjectScope, ScopedProjectId.ToString()),
new Claim(KnownClaims.CampaignScope, ScopedCampaignId.ToString()),
]);
User dev = await EnsureUserAsync(
@@ -117,6 +120,12 @@ public static class DevelopmentSeedExtensions
[
]);
await EnsureOrganizationDataAsync(
manager.Id,
dev.Id,
dbContext,
cancellationToken);
await EnsureWorkspaceDataAsync(
manager.Id,
clientUser.Id,
@@ -200,7 +209,7 @@ public static class DevelopmentSeedExtensions
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)
.Where(claim => claim.Type is KnownClaims.WorkspaceScope or KnownClaims.ClientScope or KnownClaims.CampaignScope or KnownClaims.Persona)
.ToList();
foreach (Claim claim in managedClaims)
@@ -224,6 +233,75 @@ public static class DevelopmentSeedExtensions
return user;
}
private static async Task EnsureOrganizationDataAsync(
Guid managerUserId,
Guid developerUserId,
AppDbContext dbContext,
CancellationToken cancellationToken)
{
Organization? organization = await dbContext.Organizations
.SingleOrDefaultAsync(candidate => candidate.Id == OrganizationId, cancellationToken);
if (organization is null)
{
organization = new Organization
{
Id = OrganizationId,
Name = string.Empty,
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.Organizations.Add(organization);
}
organization.Name = "Northstar Collective";
organization.OwnerUserId = managerUserId;
await UpsertOrganizationMembershipAsync(
dbContext,
Guid.Parse("99999999-9999-9999-9999-000000000001"),
OrganizationId,
managerUserId,
OrganizationRoles.Owner,
cancellationToken);
await UpsertOrganizationMembershipAsync(
dbContext,
Guid.Parse("99999999-9999-9999-9999-000000000002"),
OrganizationId,
developerUserId,
OrganizationRoles.Admin,
cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task UpsertOrganizationMembershipAsync(
AppDbContext dbContext,
Guid membershipId,
Guid organizationId,
Guid userId,
string role,
CancellationToken cancellationToken)
{
OrganizationMembership? membership = await dbContext.OrganizationMemberships
.SingleOrDefaultAsync(
candidate => candidate.OrganizationId == organizationId && candidate.UserId == userId,
cancellationToken);
if (membership is null)
{
membership = new OrganizationMembership
{
Id = membershipId,
OrganizationId = organizationId,
UserId = userId,
Role = role,
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.OrganizationMemberships.Add(membership);
}
membership.Role = role;
}
private static async Task EnsureWorkspaceDataAsync(
Guid managerUserId,
Guid clientUserId,
@@ -248,6 +326,7 @@ public static class DevelopmentSeedExtensions
workspace.Name = "Northstar Studio";
workspace.Slug = "northstar-studio";
workspace.OrganizationId = OrganizationId;
workspace.OwnerUserId = managerUserId;
workspace.TimeZone = "America/Montreal";
await dbContext.SaveChangesAsync(cancellationToken);
@@ -273,9 +352,9 @@ public static class DevelopmentSeedExtensions
WorkspaceId,
cancellationToken);
await UpsertProjectAsync(
await UpsertCampaignAsync(
dbContext,
ScopedProjectId,
ScopedCampaignId,
WorkspaceId,
ScopedClientId,
"Spring Launch",
@@ -285,9 +364,9 @@ public static class DevelopmentSeedExtensions
"Cross-channel launch campaign for the spring offer.",
"Coordinate creative approvals before the final week.",
cancellationToken);
await UpsertProjectAsync(
await UpsertCampaignAsync(
dbContext,
HiddenProjectId,
HiddenCampaignId,
WorkspaceId,
HiddenClientId,
"Summer Retention",
@@ -303,7 +382,7 @@ public static class DevelopmentSeedExtensions
ScopedContentItemId,
WorkspaceId,
ScopedClientId,
ScopedProjectId,
ScopedCampaignId,
"Spring launch hero video",
"Fresh seasonal menu launch across Instagram and TikTok.",
"Instagram Reel, TikTok",
@@ -317,7 +396,7 @@ public static class DevelopmentSeedExtensions
HiddenContentItemId,
WorkspaceId,
HiddenClientId,
HiddenProjectId,
HiddenCampaignId,
"Bakery loyalty carousel",
"Reward regular customers with a four-card retention carousel.",
"Instagram Carousel",
@@ -491,7 +570,7 @@ public static class DevelopmentSeedExtensions
await dbContext.SaveChangesAsync(cancellationToken);
}
private static async Task UpsertProjectAsync(
private static async Task UpsertCampaignAsync(
AppDbContext dbContext,
Guid id,
Guid workspaceId,
@@ -504,26 +583,26 @@ public static class DevelopmentSeedExtensions
string? notes,
CancellationToken cancellationToken)
{
Project? project = await dbContext.Projects.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
if (project is null)
Campaign? campaign = await dbContext.Campaigns.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
if (campaign is null)
{
project = new Project
campaign = new Campaign
{
Id = id,
Name = string.Empty,
Status = string.Empty,
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.Projects.Add(project);
dbContext.Campaigns.Add(campaign);
}
project.WorkspaceId = workspaceId;
project.ClientId = clientId;
project.Name = name;
project.Description = description;
project.Notes = notes;
project.Status = status;
project.StartDate = startDate;
project.EndDate = endDate;
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);
}
@@ -532,7 +611,7 @@ public static class DevelopmentSeedExtensions
Guid id,
Guid workspaceId,
Guid clientId,
Guid projectId,
Guid campaignId,
string title,
string publicationMessage,
string publicationTargets,
@@ -559,7 +638,7 @@ public static class DevelopmentSeedExtensions
}
item.WorkspaceId = workspaceId;
item.ClientId = clientId;
item.ProjectId = projectId;
item.CampaignId = campaignId;
item.Title = title;
item.PublicationMessage = publicationMessage;
item.PublicationTargets = publicationTargets;

View File

@@ -1,9 +1,11 @@
using System.Security.Claims;
using Socialize.Api.Modules.Identity.Contracts;
using Socialize.Api.Modules.Organizations.Services;
namespace Socialize.Api.Infrastructure.Security;
public sealed class AccessScopeService
public sealed class AccessScopeService(
OrganizationAccessService organizationAccessService)
{
public bool IsManager(ClaimsPrincipal user)
{
@@ -36,21 +38,140 @@ public sealed class AccessScopeService
|| (CanAccessWorkspace(user, workspaceId) && user.GetClientScopeIds().Contains(clientId));
}
public bool CanAccessProject(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
public bool CanAccessCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
{
return IsManager(user)
|| (CanAccessClient(user, workspaceId, clientId) && user.GetProjectScopeIds().Contains(projectId));
|| (CanAccessClient(user, workspaceId, clientId) && user.GetCampaignScopeIds().Contains(campaignId));
}
public bool CanContributeToProject(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
public bool CanContributeToCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
{
return IsManager(user) || (IsProvider(user) && CanAccessProject(user, workspaceId, clientId, projectId));
return IsManager(user) || (IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId));
}
public bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
public bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
{
return IsManager(user)
|| IsProvider(user) && CanAccessProject(user, workspaceId, clientId, projectId)
|| IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|| IsClient(user) && CanAccessClient(user, workspaceId, clientId);
}
public Task<IReadOnlyCollection<Guid>> GetAccessibleWorkspaceIdsAsync(
ClaimsPrincipal user,
CancellationToken ct)
{
return organizationAccessService.GetAccessibleWorkspaceIdsAsync(user, ct);
}
public async Task<bool> CanAccessWorkspaceAsync(
ClaimsPrincipal user,
Guid workspaceId,
CancellationToken ct)
{
return CanAccessWorkspace(user, workspaceId)
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user,
workspaceId,
OrganizationPermissions.AccessOwnedWorkspaces,
ct);
}
public async Task<bool> CanManageWorkspaceAsync(
ClaimsPrincipal user,
Guid workspaceId,
CancellationToken ct)
{
return IsManager(user)
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user,
workspaceId,
OrganizationPermissions.ManageWorkspaces,
ct);
}
public async Task<bool> CanCreateWorkspaceAsync(
ClaimsPrincipal user,
Guid organizationId,
CancellationToken ct)
{
return IsManager(user)
|| await organizationAccessService.HasOrganizationPermissionAsync(
user,
organizationId,
OrganizationPermissions.CreateWorkspaces,
ct);
}
public async Task<bool> CanAccessClientAsync(
ClaimsPrincipal user,
Guid workspaceId,
Guid clientId,
CancellationToken ct)
{
if (IsManager(user) ||
await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user,
workspaceId,
OrganizationPermissions.AccessOwnedWorkspaces,
ct))
{
return true;
}
return user.GetWorkspaceScopeIds().Contains(workspaceId) && user.GetClientScopeIds().Contains(clientId);
}
public async Task<bool> CanAccessCampaignAsync(
ClaimsPrincipal user,
Guid workspaceId,
Guid clientId,
Guid campaignId,
CancellationToken ct)
{
if (IsManager(user) ||
await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user,
workspaceId,
OrganizationPermissions.AccessOwnedWorkspaces,
ct))
{
return true;
}
return await CanAccessClientAsync(user, workspaceId, clientId, ct) &&
user.GetCampaignScopeIds().Contains(campaignId);
}
public async Task<bool> CanContributeToCampaignAsync(
ClaimsPrincipal user,
Guid workspaceId,
Guid clientId,
Guid campaignId,
CancellationToken ct)
{
return IsManager(user)
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user,
workspaceId,
OrganizationPermissions.ManageWorkspaces,
ct)
|| IsProvider(user) && await CanAccessCampaignAsync(user, workspaceId, clientId, campaignId, ct);
}
public async Task<bool> CanReviewContentAsync(
ClaimsPrincipal user,
Guid workspaceId,
Guid clientId,
Guid campaignId,
CancellationToken ct)
{
return IsManager(user)
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync(
user,
workspaceId,
OrganizationPermissions.AccessOwnedWorkspaces,
ct)
|| IsProvider(user) && await CanAccessCampaignAsync(user, workspaceId, clientId, campaignId, ct)
|| IsClient(user) && await CanAccessClientAsync(user, workspaceId, clientId, ct);
}
}

View File

@@ -23,9 +23,9 @@ public static class ClaimsPrincipalExtensions
return claims.GetScopeIds(KnownClaims.ClientScope);
}
public static IReadOnlyCollection<Guid> GetProjectScopeIds(this ClaimsPrincipal claims)
public static IReadOnlyCollection<Guid> GetCampaignScopeIds(this ClaimsPrincipal claims)
{
return claims.GetScopeIds(KnownClaims.ProjectScope);
return claims.GetScopeIds(KnownClaims.CampaignScope);
}
public static string? GetPersona(this ClaimsPrincipal claims)

View File

@@ -6,6 +6,6 @@ public static class KnownClaims
public const string PortraitUrl = "portraitUrl";
public const string WorkspaceScope = "workspace";
public const string ClientScope = "client";
public const string ProjectScope = "project";
public const string CampaignScope = "campaign";
public const string Persona = "persona";
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,117 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Socialize.Api.Migrations
{
/// <inheritdoc />
public partial class RenameProjectsToCampaigns : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameTable(
name: "Projects",
newName: "Campaigns");
migrationBuilder.DropPrimaryKey(
name: "PK_Projects",
table: "Campaigns");
migrationBuilder.AddPrimaryKey(
name: "PK_Campaigns",
table: "Campaigns",
column: "Id");
migrationBuilder.RenameIndex(
name: "IX_Projects_WorkspaceId",
table: "Campaigns",
newName: "IX_Campaigns_WorkspaceId");
migrationBuilder.RenameIndex(
name: "IX_Projects_ClientId_Name",
table: "Campaigns",
newName: "IX_Campaigns_ClientId_Name");
migrationBuilder.RenameIndex(
name: "IX_Projects_ClientId",
table: "Campaigns",
newName: "IX_Campaigns_ClientId");
migrationBuilder.RenameColumn(
name: "ProjectName",
table: "FeedbackReports",
newName: "CampaignName");
migrationBuilder.RenameColumn(
name: "ProjectId",
table: "FeedbackReports",
newName: "CampaignId");
migrationBuilder.RenameColumn(
name: "ProjectId",
table: "ContentItems",
newName: "CampaignId");
migrationBuilder.RenameIndex(
name: "IX_ContentItems_ProjectId",
table: "ContentItems",
newName: "IX_ContentItems_CampaignId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_Campaigns",
table: "Campaigns");
migrationBuilder.AddPrimaryKey(
name: "PK_Projects",
table: "Campaigns",
column: "Id");
migrationBuilder.RenameIndex(
name: "IX_Campaigns_WorkspaceId",
table: "Campaigns",
newName: "IX_Projects_WorkspaceId");
migrationBuilder.RenameIndex(
name: "IX_Campaigns_ClientId_Name",
table: "Campaigns",
newName: "IX_Projects_ClientId_Name");
migrationBuilder.RenameIndex(
name: "IX_Campaigns_ClientId",
table: "Campaigns",
newName: "IX_Projects_ClientId");
migrationBuilder.RenameTable(
name: "Campaigns",
newName: "Projects");
migrationBuilder.RenameColumn(
name: "CampaignName",
table: "FeedbackReports",
newName: "ProjectName");
migrationBuilder.RenameColumn(
name: "CampaignId",
table: "FeedbackReports",
newName: "ProjectId");
migrationBuilder.RenameColumn(
name: "CampaignId",
table: "ContentItems",
newName: "ProjectId");
migrationBuilder.RenameIndex(
name: "IX_ContentItems_CampaignId",
table: "ContentItems",
newName: "IX_ContentItems_ProjectId");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,126 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Socialize.Api.Migrations
{
/// <inheritdoc />
public partial class AddOrganizations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "OrganizationId",
table: "Workspaces",
type: "uuid",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.CreateTable(
name: "Organizations",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
OwnerUserId = table.Column<Guid>(type: "uuid", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
},
constraints: table =>
{
table.PrimaryKey("PK_Organizations", x => x.Id);
});
migrationBuilder.CreateTable(
name: "OrganizationMemberships",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
UserId = table.Column<Guid>(type: "uuid", nullable: false),
Role = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
},
constraints: table =>
{
table.PrimaryKey("PK_OrganizationMemberships", x => x.Id);
table.ForeignKey(
name: "FK_OrganizationMemberships_Organizations_OrganizationId",
column: x => x.OrganizationId,
principalTable: "Organizations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.Sql(
"""
INSERT INTO "Organizations" ("Id", "Name", "OwnerUserId", "CreatedAt")
VALUES ('99999999-9999-9999-9999-999999999999', 'Northstar Collective', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', CURRENT_TIMESTAMP);
UPDATE "Workspaces"
SET "OrganizationId" = '99999999-9999-9999-9999-999999999999'
WHERE "OrganizationId" = '00000000-0000-0000-0000-000000000000';
INSERT INTO "OrganizationMemberships" ("Id", "OrganizationId", "UserId", "Role", "CreatedAt")
VALUES ('99999999-9999-9999-9999-000000000001', '99999999-9999-9999-9999-999999999999', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Owner', CURRENT_TIMESTAMP);
""");
migrationBuilder.CreateIndex(
name: "IX_Workspaces_OrganizationId",
table: "Workspaces",
column: "OrganizationId");
migrationBuilder.CreateIndex(
name: "IX_OrganizationMemberships_OrganizationId",
table: "OrganizationMemberships",
column: "OrganizationId");
migrationBuilder.CreateIndex(
name: "IX_OrganizationMemberships_OrganizationId_UserId",
table: "OrganizationMemberships",
columns: new[] { "OrganizationId", "UserId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OrganizationMemberships_UserId",
table: "OrganizationMemberships",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Organizations_OwnerUserId",
table: "Organizations",
column: "OwnerUserId");
migrationBuilder.AddForeignKey(
name: "FK_Workspaces_Organizations_OrganizationId",
table: "Workspaces",
column: "OrganizationId",
principalTable: "Organizations",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Workspaces_Organizations_OrganizationId",
table: "Workspaces");
migrationBuilder.DropTable(
name: "OrganizationMemberships");
migrationBuilder.DropTable(
name: "Organizations");
migrationBuilder.DropIndex(
name: "IX_Workspaces_OrganizationId",
table: "Workspaces");
migrationBuilder.DropColumn(
name: "OrganizationId",
table: "Workspaces");
}
}
}

View File

@@ -438,6 +438,59 @@ namespace Socialize.Api.Migrations
b.ToTable("AssetRevisions", (string)null);
});
modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ClientId")
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<string>("Description")
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.Property<DateTimeOffset>("EndDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Notes")
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.Property<DateTimeOffset>("StartDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<Guid>("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.Clients.Data.Client", b =>
{
b.Property<Guid>("Id")
@@ -549,6 +602,9 @@ namespace Socialize.Api.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("CampaignId")
.HasColumnType("uuid");
b.Property<Guid>("ClientId")
.HasColumnType("uuid");
@@ -572,9 +628,6 @@ namespace Socialize.Api.Migrations
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<Guid>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("PublicationMessage")
.IsRequired()
.HasMaxLength(4000)
@@ -600,9 +653,9 @@ namespace Socialize.Api.Migrations
b.HasKey("Id");
b.HasIndex("ClientId");
b.HasIndex("CampaignId");
b.HasIndex("ProjectId");
b.HasIndex("ClientId");
b.HasIndex("WorkspaceId");
@@ -784,6 +837,13 @@ namespace Socialize.Api.Migrations
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<Guid?>("CampaignId")
.HasColumnType("uuid");
b.Property<string>("CampaignName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("CancellationReason")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)");
@@ -821,13 +881,6 @@ namespace Socialize.Api.Migrations
b.Property<DateTimeOffset>("LastActivityAt")
.HasColumnType("timestamp with time zone");
b.Property<Guid?>("ProjectId")
.HasColumnType("uuid");
b.Property<string>("ProjectName")
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("ReporterDisplayName")
.IsRequired()
.HasMaxLength(256)
@@ -1150,57 +1203,64 @@ namespace Socialize.Api.Migrations
b.ToTable("NotificationEvents", (string)null);
});
modelBuilder.Entity("Socialize.Api.Modules.Projects.Data.Project", b =>
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.Organization", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<Guid>("ClientId")
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<string>("Description")
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.Property<DateTimeOffset>("EndDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<string>("Notes")
.HasMaxLength(4000)
.HasColumnType("character varying(4000)");
b.Property<DateTimeOffset>("StartDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<Guid>("WorkspaceId")
b.Property<Guid>("OwnerUserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("ClientId");
b.HasIndex("OwnerUserId");
b.HasIndex("WorkspaceId");
b.ToTable("Organizations", (string)null);
});
b.HasIndex("ClientId", "Name")
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<string>("Role")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<Guid>("UserId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("OrganizationId");
b.HasIndex("UserId");
b.HasIndex("OrganizationId", "UserId")
.IsUnique();
b.ToTable("Projects", (string)null);
b.ToTable("OrganizationMemberships", (string)null);
});
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
@@ -1235,6 +1295,9 @@ namespace Socialize.Api.Migrations
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<Guid>("OwnerUserId")
.HasColumnType("uuid");
@@ -1260,6 +1323,8 @@ namespace Socialize.Api.Migrations
b.HasKey("Id");
b.HasIndex("OrganizationId");
b.HasIndex("OwnerUserId");
b.HasIndex("Slug")
@@ -1404,6 +1469,24 @@ namespace Socialize.Api.Migrations
b.Navigation("FeedbackReport");
});
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.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.Feedback.Data.FeedbackReport", b =>
{
b.Navigation("ActivityEntries");

View File

@@ -1,139 +0,0 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Approvals.Data;
using Socialize.Api.Modules.Approvals.Services;
using Socialize.Api.Modules.Notifications.Contracts;
using Socialize.Api.Modules.Workspaces.Data;
namespace Socialize.Api.Modules.Approvals.Handlers;
public record CreateApprovalRequestRequest(
Guid WorkspaceId,
Guid ContentItemId,
string Stage,
string ReviewerName,
string ReviewerEmail,
DateTimeOffset? DueAt);
public class CreateApprovalRequestRequestValidator
: Validator<CreateApprovalRequestRequest>
{
public CreateApprovalRequestRequestValidator()
{
RuleFor(x => x.WorkspaceId).NotEmpty();
RuleFor(x => x.ContentItemId).NotEmpty();
RuleFor(x => x.Stage).NotEmpty().MaximumLength(64);
RuleFor(x => x.ReviewerName).NotEmpty().MaximumLength(256);
RuleFor(x => x.ReviewerEmail).NotEmpty().MaximumLength(256).EmailAddress();
}
}
public class CreateApprovalRequestHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService,
INotificationEventWriter notificationEventWriter)
: Endpoint<CreateApprovalRequestRequest, ApprovalRequestDto>
{
public override void Configure()
{
Post("/api/approvals");
Options(o => o.WithTags("Approvals"));
}
public override async Task HandleAsync(CreateApprovalRequestRequest request, CancellationToken ct)
{
var contentItem = await dbContext
.ContentItems
.SingleOrDefaultAsync(
candidate => candidate.Id == request.ContentItemId && candidate.WorkspaceId == request.WorkspaceId,
ct);
if (contentItem is null)
{
AddError(request => request.ContentItemId, "The selected content item does not exist in the active workspace.");
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
return;
}
if (!accessScopeService.CanManageWorkspace(User, contentItem.WorkspaceId))
{
await SendForbiddenAsync(ct);
return;
}
Workspace? workspace = await dbContext.Workspaces.SingleOrDefaultAsync(candidate => candidate.Id == contentItem.WorkspaceId, ct);
if (workspace is null)
{
await SendNotFoundAsync(ct);
return;
}
if (!ApprovalWorkflowRules.CanCreateSingleStepApprovalRequest(workspace.ApprovalMode))
{
AddError(request => request.WorkspaceId, workspace.ApprovalMode == ApprovalModes.None
? "Approval workflow is disabled for this workspace."
: "Move content to In approval to start the configured multi-level approval workflow.");
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
return;
}
var approval = new ApprovalRequest()
{
Id = Guid.NewGuid(),
WorkspaceId = request.WorkspaceId,
ContentItemId = request.ContentItemId,
Stage = request.Stage.Trim(),
ReviewerName = request.ReviewerName.Trim(),
ReviewerEmail = request.ReviewerEmail.Trim(),
RequestedByUserId = User.GetUserId(),
DueAt = request.DueAt,
State = "Pending",
AccessToken = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant(),
SentAt = DateTimeOffset.UtcNow,
};
dbContext.ApprovalRequests.Add(approval);
contentItem.Status = "In approval";
await dbContext.SaveChangesAsync(ct);
await notificationEventWriter.WriteAsync(
new NotificationEventWriteModel(
approval.WorkspaceId,
approval.ContentItemId,
"approval.requested",
"ApprovalRequest",
approval.Id,
$"Approval requested from {approval.ReviewerName} for {contentItem.Title}.",
null,
approval.ReviewerEmail,
$$"""{"stage":"{{approval.Stage}}","accessToken":"{{approval.AccessToken}}"}"""),
ct);
ApprovalRequestDto dto = new(
approval.Id,
approval.WorkspaceId,
approval.ContentItemId,
approval.WorkflowInstanceId,
approval.WorkflowStepSortOrder,
approval.WorkflowStepTargetType,
approval.WorkflowStepTargetValue,
approval.WorkflowStepRequiredApproverCount,
approval.Stage,
approval.ReviewerName,
approval.ReviewerEmail,
approval.RequestedByUserId,
approval.DueAt,
approval.State,
approval.AccessToken,
approval.SentAt,
approval.CompletedAt,
[]);
await SendAsync(dto, StatusCodes.Status201Created, ct);
}
}

View File

@@ -61,7 +61,7 @@ public class GetApprovalsHandler(
return;
}
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -12,7 +12,6 @@ namespace Socialize.Api.Modules.Approvals.Handlers;
public record SubmitApprovalDecisionRequest(
string Decision,
string? Comment,
string? ReviewerName,
string? ReviewerEmail);
@@ -25,7 +24,6 @@ public class SubmitApprovalDecisionRequestValidator
.NotEmpty()
.Equal("Approved")
.WithMessage("Only approved decisions are supported.");
RuleFor(x => x.Comment).MaximumLength(2048);
RuleFor(x => x.ReviewerName).MaximumLength(256);
RuleFor(x => x.ReviewerEmail).MaximumLength(256).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ReviewerEmail));
}
@@ -64,7 +62,7 @@ public class SubmitApprovalDecisionHandler(
}
if (User?.Identity?.IsAuthenticated == true &&
!accessScopeService.CanReviewContent(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
!await accessScopeService.CanReviewContentAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;
@@ -90,7 +88,7 @@ public class SubmitApprovalDecisionHandler(
Id = Guid.NewGuid(),
ApprovalRequestId = approval.Id,
Decision = normalizedDecision,
Comment = string.IsNullOrWhiteSpace(request.Comment) ? null : request.Comment.Trim(),
Comment = null,
DecidedByUserId = User?.Identity?.IsAuthenticated == true ? User.GetUserId() : null,
DecidedByName = decidedByName,
DecidedByEmail = decidedByEmail,

View File

@@ -12,11 +12,6 @@ public static class ApprovalModes
public static class ApprovalWorkflowRules
{
public static bool CanCreateSingleStepApprovalRequest(string approvalMode)
{
return approvalMode is ApprovalModes.Optional or ApprovalModes.Required;
}
public static bool BlocksManualApprovedOrScheduledStatus(string approvalMode)
{
return approvalMode is ApprovalModes.Required or ApprovalModes.MultiLevel;

View File

@@ -51,7 +51,7 @@ public class CreateAssetRevisionHandler(
.SingleOrDefaultAsync(candidate => candidate.Id == asset.ContentItemId, ct);
if (contentItem is not null &&
!accessScopeService.CanContributeToProject(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
!await accessScopeService.CanContributeToCampaignAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -58,7 +58,7 @@ public class CreateGoogleDriveAssetHandler(
return;
}
if (!accessScopeService.CanContributeToProject(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
if (!await accessScopeService.CanContributeToCampaignAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -52,7 +52,7 @@ public class GetAssetsHandler(
return;
}
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -1,6 +1,6 @@
namespace Socialize.Api.Modules.Projects.Data;
namespace Socialize.Api.Modules.Campaigns.Data;
public class Project
public class Campaign
{
public Guid Id { get; init; }
public Guid WorkspaceId { get; set; }

View File

@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore;
namespace Socialize.Api.Modules.Campaigns.Data;
public static class CampaignModelConfiguration
{
public static ModelBuilder ConfigureCampaignsModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<Campaign>(campaign =>
{
campaign.ToTable("Campaigns");
campaign.HasKey(x => x.Id);
campaign.Property(x => x.Name).HasMaxLength(256).IsRequired();
campaign.Property(x => x.Description).HasMaxLength(4000);
campaign.Property(x => x.Notes).HasMaxLength(4000);
campaign.Property(x => x.Status).HasMaxLength(64).IsRequired();
campaign.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
campaign.HasIndex(x => new { x.ClientId, x.Name }).IsUnique();
campaign.HasIndex(x => x.WorkspaceId);
campaign.HasIndex(x => x.ClientId);
});
return modelBuilder;
}
}

View File

@@ -0,0 +1,12 @@
using Socialize.Api.Modules.Campaigns.Data;
namespace Socialize.Api.Modules.Campaigns;
public static class DependencyInjection
{
public static WebApplicationBuilder AddCampaignsModule(
this WebApplicationBuilder builder)
{
return builder;
}
}

View File

@@ -2,11 +2,11 @@ using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Projects.Data;
using Socialize.Api.Modules.Campaigns.Data;
namespace Socialize.Api.Modules.Projects.Handlers;
namespace Socialize.Api.Modules.Campaigns.Handlers;
public record CreateProjectRequest(
public record CreateCampaignRequest(
Guid WorkspaceId,
Guid ClientId,
string Name,
@@ -15,10 +15,10 @@ public record CreateProjectRequest(
string? Description,
string? Notes);
public class CreateProjectRequestValidator
: Validator<CreateProjectRequest>
public class CreateCampaignRequestValidator
: Validator<CreateCampaignRequest>
{
public CreateProjectRequestValidator()
public CreateCampaignRequestValidator()
{
RuleFor(x => x.WorkspaceId).NotEmpty();
RuleFor(x => x.ClientId).NotEmpty();
@@ -32,20 +32,20 @@ public class CreateProjectRequestValidator
}
}
public class CreateProjectHandler(
public class CreateCampaignHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService)
: Endpoint<CreateProjectRequest, ProjectDto>
: Endpoint<CreateCampaignRequest, CampaignDto>
{
public override void Configure()
{
Post("/api/projects");
Options(o => o.WithTags("Projects"));
Post("/api/campaigns");
Options(o => o.WithTags("Campaigns"));
}
public override async Task HandleAsync(CreateProjectRequest request, CancellationToken ct)
public override async Task HandleAsync(CreateCampaignRequest request, CancellationToken ct)
{
if (!accessScopeService.CanManageWorkspace(User, request.WorkspaceId))
if (!await accessScopeService.CanManageWorkspaceAsync(User, request.WorkspaceId, ct))
{
await SendForbiddenAsync(ct);
return;
@@ -75,19 +75,19 @@ public class CreateProjectHandler(
string normalizedName = request.Name.Trim();
bool duplicateProject = await dbContext.Projects
bool duplicateCampaign = await dbContext.Campaigns
.AnyAsync(
project => project.ClientId == request.ClientId && project.Name == normalizedName,
campaign => campaign.ClientId == request.ClientId && campaign.Name == normalizedName,
ct);
if (duplicateProject)
if (duplicateCampaign)
{
AddError(request => request.Name, "A project with this name already exists for the selected client.");
AddError(request => request.Name, "A campaign with this name already exists for the selected client.");
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
return;
}
Project project = new()
Campaign campaign = new()
{
Id = Guid.NewGuid(),
WorkspaceId = request.WorkspaceId,
@@ -101,19 +101,19 @@ public class CreateProjectHandler(
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.Projects.Add(project);
dbContext.Campaigns.Add(campaign);
await dbContext.SaveChangesAsync(ct);
ProjectDto dto = new(
project.Id,
project.WorkspaceId,
project.ClientId,
project.Name,
project.Description,
project.Notes,
project.Status,
project.StartDate,
project.EndDate);
CampaignDto dto = new(
campaign.Id,
campaign.WorkspaceId,
campaign.ClientId,
campaign.Name,
campaign.Description,
campaign.Notes,
campaign.Status,
campaign.StartDate,
campaign.EndDate);
await SendAsync(dto, StatusCodes.Status201Created, ct);
}

View File

@@ -0,0 +1,82 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Campaigns.Data;
namespace Socialize.Api.Modules.Campaigns.Handlers;
public record GetCampaignsRequest(Guid? WorkspaceId, Guid? ClientId);
public record CampaignDto(
Guid Id,
Guid WorkspaceId,
Guid ClientId,
string Name,
string? Description,
string? Notes,
string Status,
DateTimeOffset StartDate,
DateTimeOffset EndDate);
public class GetCampaignsHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService)
: Endpoint<GetCampaignsRequest, IReadOnlyCollection<CampaignDto>>
{
public override void Configure()
{
Get("/api/campaigns");
Options(o => o.WithTags("Campaigns"));
}
public override async Task HandleAsync(GetCampaignsRequest request, CancellationToken ct)
{
IQueryable<Campaign> query = dbContext.Campaigns.AsQueryable();
if (!accessScopeService.IsManager(User))
{
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
query = query.Where(campaign => workspaceScopeIds.Contains(campaign.WorkspaceId));
if (clientScopeIds.Count > 0)
{
query = query.Where(campaign => clientScopeIds.Contains(campaign.ClientId));
}
if (campaignScopeIds.Count > 0)
{
query = query.Where(campaign => campaignScopeIds.Contains(campaign.Id));
}
}
if (request.ClientId.HasValue)
{
query = query.Where(campaign => campaign.ClientId == request.ClientId.Value);
}
if (request.WorkspaceId.HasValue)
{
query = query.Where(campaign => campaign.WorkspaceId == request.WorkspaceId.Value);
}
List<CampaignDto> campaigns = await query
.OrderBy(campaign => campaign.Name)
.Select(campaign => new CampaignDto(
campaign.Id,
campaign.WorkspaceId,
campaign.ClientId,
campaign.Name,
campaign.Description,
campaign.Notes,
campaign.Status,
campaign.StartDate,
campaign.EndDate))
.ToListAsync(ct);
await SendOkAsync(campaigns, ct);
}
}

View File

@@ -47,7 +47,7 @@ public class ChangeClientPortraitHandler(
return;
}
if (!accessScopeService.CanManageWorkspace(User, client.WorkspaceId))
if (!await accessScopeService.CanManageWorkspaceAsync(User, client.WorkspaceId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -41,7 +41,7 @@ public class CreateClientHandler(
public override async Task HandleAsync(CreateClientRequest request, CancellationToken ct)
{
if (!accessScopeService.CanManageWorkspace(User, request.WorkspaceId))
if (!await accessScopeService.CanManageWorkspaceAsync(User, request.WorkspaceId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -33,16 +33,9 @@ public class GetClientsHandler(
{
IQueryable<Client> query = dbContext.Clients.AsQueryable();
if (accessScopeService.IsManager(User))
if (!accessScopeService.IsManager(User))
{
if (request.WorkspaceId.HasValue)
{
query = query.Where(client => client.WorkspaceId == request.WorkspaceId.Value);
}
}
else
{
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
query = query.Where(client => workspaceScopeIds.Contains(client.WorkspaceId));
@@ -52,10 +45,11 @@ public class GetClientsHandler(
query = query.Where(client => clientScopeIds.Contains(client.Id));
}
if (request.WorkspaceId.HasValue)
{
query = query.Where(client => client.WorkspaceId == request.WorkspaceId.Value);
}
}
if (request.WorkspaceId.HasValue)
{
query = query.Where(client => client.WorkspaceId == request.WorkspaceId.Value);
}
List<ClientDto> clients = await query

View File

@@ -50,7 +50,7 @@ public class UpdateClientHandler(
return;
}
if (!accessScopeService.CanManageWorkspace(User, client.WorkspaceId))
if (!await accessScopeService.CanManageWorkspaceAsync(User, client.WorkspaceId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -51,7 +51,7 @@ public class CreateCommentHandler(
return;
}
if (!accessScopeService.CanReviewContent(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
if (!await accessScopeService.CanReviewContentAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -44,7 +44,7 @@ public class GetCommentsHandler(
return;
}
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -39,8 +39,8 @@ public class ResolveCommentHandler(
return;
}
bool canResolve = accessScopeService.CanManageWorkspace(User, comment.WorkspaceId)
|| accessScopeService.CanContributeToProject(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId);
bool canResolve = await accessScopeService.CanManageWorkspaceAsync(User, comment.WorkspaceId, ct)
|| await accessScopeService.CanContributeToCampaignAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct);
if (!canResolve)
{

View File

@@ -5,7 +5,7 @@ public class ContentItem
public Guid Id { get; init; }
public Guid WorkspaceId { get; set; }
public Guid ClientId { get; set; }
public Guid ProjectId { get; set; }
public Guid CampaignId { get; set; }
public required string Title { get; set; }
public required string PublicationMessage { get; set; }
public required string PublicationTargets { get; set; }

View File

@@ -21,7 +21,7 @@ public static class ContentItemModelConfiguration
.HasDefaultValueSql("CURRENT_TIMESTAMP");
contentItem.HasIndex(x => x.WorkspaceId);
contentItem.HasIndex(x => x.ClientId);
contentItem.HasIndex(x => x.ProjectId);
contentItem.HasIndex(x => x.CampaignId);
});
modelBuilder.Entity<ContentItemRevision>(revision =>

View File

@@ -11,7 +11,7 @@ namespace Socialize.Api.Modules.ContentItems.Handlers;
public record CreateContentItemRequest(
Guid WorkspaceId,
Guid ClientId,
Guid ProjectId,
Guid CampaignId,
string Title,
string PublicationMessage,
string PublicationTargets,
@@ -25,7 +25,7 @@ public class CreateContentItemRequestValidator
{
RuleFor(x => x.WorkspaceId).NotEmpty();
RuleFor(x => x.ClientId).NotEmpty();
RuleFor(x => x.ProjectId).NotEmpty();
RuleFor(x => x.CampaignId).NotEmpty();
RuleFor(x => x.Title).NotEmpty().MaximumLength(256);
RuleFor(x => x.PublicationMessage).NotEmpty().MaximumLength(4000);
RuleFor(x => x.PublicationTargets).NotEmpty().MaximumLength(512);
@@ -47,7 +47,7 @@ public class CreateContentItemHandler(
public override async Task HandleAsync(CreateContentItemRequest request, CancellationToken ct)
{
if (!accessScopeService.CanContributeToProject(User, request.WorkspaceId, request.ClientId, request.ProjectId))
if (!await accessScopeService.CanContributeToCampaignAsync(User, request.WorkspaceId, request.ClientId, request.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;
@@ -75,16 +75,16 @@ public class CreateContentItemHandler(
return;
}
bool projectExists = await dbContext.Projects
bool campaignExists = await dbContext.Campaigns
.AnyAsync(
project => project.Id == request.ProjectId &&
project.WorkspaceId == request.WorkspaceId &&
project.ClientId == request.ClientId,
campaign => campaign.Id == request.CampaignId &&
campaign.WorkspaceId == request.WorkspaceId &&
campaign.ClientId == request.ClientId,
ct);
if (!projectExists)
if (!campaignExists)
{
AddError(request => request.ProjectId, "The selected project does not belong to the selected client.");
AddError(request => request.CampaignId, "The selected campaign does not belong to the selected client.");
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
return;
}
@@ -94,7 +94,7 @@ public class CreateContentItemHandler(
Id = Guid.NewGuid(),
WorkspaceId = request.WorkspaceId,
ClientId = request.ClientId,
ProjectId = request.ProjectId,
CampaignId = request.CampaignId,
Title = request.Title.Trim(),
PublicationMessage = request.PublicationMessage.Trim(),
PublicationTargets = request.PublicationTargets.Trim(),
@@ -138,7 +138,7 @@ public class CreateContentItemHandler(
item.Id,
item.WorkspaceId,
item.ClientId,
item.ProjectId,
item.CampaignId,
item.Title,
item.PublicationMessage,
item.PublicationTargets,

View File

@@ -50,7 +50,7 @@ public class CreateContentItemRevisionHandler(
return;
}
if (!accessScopeService.CanContributeToProject(User, item.WorkspaceId, item.ClientId, item.ProjectId))
if (!await accessScopeService.CanContributeToCampaignAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -10,7 +10,7 @@ public record ContentItemDetailDto(
Guid Id,
Guid WorkspaceId,
Guid ClientId,
Guid ProjectId,
Guid CampaignId,
string Title,
string PublicationMessage,
string PublicationTargets,
@@ -42,7 +42,7 @@ public class GetContentItemHandler(
candidate.Id,
candidate.WorkspaceId,
candidate.ClientId,
candidate.ProjectId,
candidate.CampaignId,
candidate.Title,
candidate.PublicationMessage,
candidate.PublicationTargets,
@@ -60,7 +60,7 @@ public class GetContentItemHandler(
return;
}
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -41,7 +41,7 @@ public class GetContentItemRevisionsHandler(
return;
}
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -6,13 +6,13 @@ using Socialize.Api.Modules.ContentItems.Data;
namespace Socialize.Api.Modules.ContentItems.Handlers;
public record GetContentItemsRequest(Guid? WorkspaceId, Guid? ClientId, Guid? ProjectId);
public record GetContentItemsRequest(Guid? WorkspaceId, Guid? ClientId, Guid? CampaignId);
public record ContentItemDto(
Guid Id,
Guid WorkspaceId,
Guid ClientId,
Guid ProjectId,
Guid CampaignId,
string Title,
string PublicationMessage,
string PublicationTargets,
@@ -39,9 +39,9 @@ public class GetContentItemsHandler(
if (!accessScopeService.IsManager(User))
{
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
IReadOnlyCollection<Guid> projectScopeIds = User.GetProjectScopeIds();
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
query = query.Where(item => workspaceScopeIds.Contains(item.WorkspaceId));
@@ -50,9 +50,9 @@ public class GetContentItemsHandler(
query = query.Where(item => clientScopeIds.Contains(item.ClientId));
}
if (projectScopeIds.Count > 0)
if (campaignScopeIds.Count > 0)
{
query = query.Where(item => projectScopeIds.Contains(item.ProjectId));
query = query.Where(item => campaignScopeIds.Contains(item.CampaignId));
}
}
@@ -61,9 +61,9 @@ public class GetContentItemsHandler(
query = query.Where(item => item.WorkspaceId == request.WorkspaceId.Value);
}
if (request.ProjectId.HasValue)
if (request.CampaignId.HasValue)
{
query = query.Where(item => item.ProjectId == request.ProjectId.Value);
query = query.Where(item => item.CampaignId == request.CampaignId.Value);
}
if (request.ClientId.HasValue)
@@ -78,7 +78,7 @@ public class GetContentItemsHandler(
item.Id,
item.WorkspaceId,
item.ClientId,
item.ProjectId,
item.CampaignId,
item.Title,
item.PublicationMessage,
item.PublicationTargets,

View File

@@ -54,7 +54,7 @@ public class UpdateContentItemStatusHandler(
return;
}
if (!accessScopeService.CanManageWorkspace(User, item.WorkspaceId))
if (!await accessScopeService.CanManageWorkspaceAsync(User, item.WorkspaceId, ct))
{
await SendForbiddenAsync(ct);
return;
@@ -145,7 +145,7 @@ public class UpdateContentItemStatusHandler(
item.Id,
item.WorkspaceId,
item.ClientId,
item.ProjectId,
item.CampaignId,
item.Title,
item.PublicationMessage,
item.PublicationTargets,

View File

@@ -7,8 +7,8 @@ public record FeedbackContextDto(
string? WorkspaceName,
Guid? ClientId,
string? ClientName,
Guid? ProjectId,
string? ProjectName,
Guid? CampaignId,
string? CampaignName,
Guid? ContentItemId,
string? ContentItemTitle);
@@ -82,8 +82,8 @@ public static class FeedbackDtoMapper
report.WorkspaceName,
report.ClientId,
report.ClientName,
report.ProjectId,
report.ProjectName,
report.CampaignId,
report.CampaignName,
report.ContentItemId,
report.ContentItemTitle),
report.Screenshot is null

View File

@@ -20,7 +20,7 @@ public static class FeedbackModelConfiguration
feedback.Property(x => x.AppVersion).HasMaxLength(128);
feedback.Property(x => x.WorkspaceName).HasMaxLength(256);
feedback.Property(x => x.ClientName).HasMaxLength(256);
feedback.Property(x => x.ProjectName).HasMaxLength(256);
feedback.Property(x => x.CampaignName).HasMaxLength(256);
feedback.Property(x => x.ContentItemTitle).HasMaxLength(256);
feedback.Property(x => x.CancellationReason).HasMaxLength(2000);
feedback.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");

View File

@@ -18,8 +18,8 @@ public class FeedbackReport
public string? WorkspaceName { get; set; }
public Guid? ClientId { get; set; }
public string? ClientName { get; set; }
public Guid? ProjectId { get; set; }
public string? ProjectName { get; set; }
public Guid? CampaignId { get; set; }
public string? CampaignName { get; set; }
public Guid? ContentItemId { get; set; }
public string? ContentItemTitle { get; set; }
public DateTimeOffset CreatedAt { get; set; }

View File

@@ -19,8 +19,8 @@ public record SubmitFeedbackRequest(
string? WorkspaceName,
Guid? ClientId,
string? ClientName,
Guid? ProjectId,
string? ProjectName,
Guid? CampaignId,
string? CampaignName,
Guid? ContentItemId,
string? ContentItemTitle);
@@ -36,7 +36,7 @@ public class SubmitFeedbackRequestValidator
RuleFor(x => x.AppVersion).MaximumLength(128);
RuleFor(x => x.WorkspaceName).MaximumLength(256);
RuleFor(x => x.ClientName).MaximumLength(256);
RuleFor(x => x.ProjectName).MaximumLength(256);
RuleFor(x => x.CampaignName).MaximumLength(256);
RuleFor(x => x.ContentItemTitle).MaximumLength(256);
RuleFor(x => x.ViewportWidth).GreaterThan(0).When(x => x.ViewportWidth.HasValue);
RuleFor(x => x.ViewportHeight).GreaterThan(0).When(x => x.ViewportHeight.HasValue);
@@ -82,8 +82,8 @@ public class SubmitFeedbackHandler(
WorkspaceName = NormalizeOptional(request.WorkspaceName),
ClientId = request.ClientId,
ClientName = NormalizeOptional(request.ClientName),
ProjectId = request.ProjectId,
ProjectName = NormalizeOptional(request.ProjectName),
CampaignId = request.CampaignId,
CampaignName = NormalizeOptional(request.CampaignName),
ContentItemId = request.ContentItemId,
ContentItemTitle = NormalizeOptional(request.ContentItemTitle),
CreatedAt = now,

View File

@@ -50,8 +50,8 @@ public class GetCurrentUserQueryHandler(
.Distinct()
.ToList();
List<Guid> projectIds = claims
.Where(claim => claim.Type == KnownClaims.ProjectScope)
List<Guid> campaignIds = claims
.Where(claim => claim.Type == KnownClaims.CampaignScope)
.Select(claim => Guid.TryParse(claim.Value, out Guid id) ? id : Guid.Empty)
.Where(id => id != Guid.Empty)
.Distinct()
@@ -64,7 +64,7 @@ public class GetCurrentUserQueryHandler(
Persona = persona,
AuthorizedWorkspaceIds = workspaceIds,
AuthorizedClientIds = clientIds,
AuthorizedProjectIds = projectIds,
AuthorizedCampaignIds = campaignIds,
Alias = userModel.Alias,
PortraitUrl = userModel.PortraitUrl,
Firstname = userModel.Firstname,

View File

@@ -7,7 +7,7 @@ public class UserDto
public string? Persona { get; init; }
public IList<Guid> AuthorizedWorkspaceIds { get; init; } = [];
public IList<Guid> AuthorizedClientIds { get; init; } = [];
public IList<Guid> AuthorizedProjectIds { get; init; } = [];
public IList<Guid> AuthorizedCampaignIds { get; init; } = [];
public string Username { get; init; } = null!;
public string? Alias { get; init; }
public string? PortraitUrl { get; init; }

View File

@@ -46,7 +46,7 @@ public class GetNotificationsHandler(
return;
}
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
{
await SendForbiddenAsync(ct);
return;
@@ -58,7 +58,7 @@ public class GetNotificationsHandler(
if (!accessScopeService.IsManager(User))
{
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
query = query.Where(notificationEvent =>
workspaceScopeIds.Contains(notificationEvent.WorkspaceId) ||
notificationEvent.RecipientUserId == currentUserId);

View File

@@ -30,7 +30,7 @@ public class MarkNotificationAsReadHandler(
Guid currentUserId = User.GetUserId();
bool canReadRecipientNotification = notificationEvent.RecipientUserId == currentUserId;
if (!canReadRecipientNotification && !accessScopeService.CanAccessWorkspace(User, notificationEvent.WorkspaceId))
if (!canReadRecipientNotification && !await accessScopeService.CanAccessWorkspaceAsync(User, notificationEvent.WorkspaceId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -0,0 +1,9 @@
namespace Socialize.Api.Modules.Organizations.Data;
public class Organization
{
public Guid Id { get; init; }
public required string Name { get; set; }
public Guid OwnerUserId { get; set; }
public DateTimeOffset CreatedAt { get; init; }
}

View File

@@ -0,0 +1,10 @@
namespace Socialize.Api.Modules.Organizations.Data;
public class OrganizationMembership
{
public Guid Id { get; init; }
public Guid OrganizationId { get; set; }
public Guid UserId { get; set; }
public required string Role { get; set; }
public DateTimeOffset CreatedAt { get; init; }
}

View File

@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
namespace Socialize.Api.Modules.Organizations.Data;
public static class OrganizationModelConfiguration
{
public static ModelBuilder ConfigureOrganizationsModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<Organization>(organization =>
{
organization.ToTable("Organizations");
organization.HasKey(x => x.Id);
organization.Property(x => x.Name).HasMaxLength(256).IsRequired();
organization.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
organization.HasIndex(x => x.OwnerUserId);
});
modelBuilder.Entity<OrganizationMembership>(membership =>
{
membership.ToTable("OrganizationMemberships");
membership.HasKey(x => x.Id);
membership.Property(x => x.Role).HasMaxLength(64).IsRequired();
membership.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
membership.HasIndex(x => x.OrganizationId);
membership.HasIndex(x => x.UserId);
membership.HasIndex(x => new { x.OrganizationId, x.UserId }).IsUnique();
membership.HasOne<Organization>()
.WithMany()
.HasForeignKey(x => x.OrganizationId)
.OnDelete(DeleteBehavior.Cascade);
});
return modelBuilder;
}
}

View File

@@ -0,0 +1,14 @@
using Socialize.Api.Modules.Organizations.Services;
namespace Socialize.Api.Modules.Organizations;
public static class DependencyInjection
{
public static WebApplicationBuilder AddOrganizationsModule(
this WebApplicationBuilder builder)
{
builder.Services.AddScoped<OrganizationAccessService>();
return builder;
}
}

View File

@@ -0,0 +1,114 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Modules.Identity.Data;
using Socialize.Api.Modules.Organizations.Data;
using Socialize.Api.Modules.Organizations.Services;
using Socialize.Api.Modules.Workspaces.Handlers;
namespace Socialize.Api.Modules.Organizations.Handlers;
public class GetOrganizationHandler(
AppDbContext dbContext,
OrganizationAccessService organizationAccessService)
: EndpointWithoutRequest<OrganizationDto>
{
public override void Configure()
{
Get("/api/organizations/{organizationId:guid}");
Options(o => o.WithTags("Organizations"));
}
public override async Task HandleAsync(CancellationToken ct)
{
Guid organizationId = Route<Guid>("organizationId");
Organization? organization = await dbContext.Organizations
.SingleOrDefaultAsync(candidate => candidate.Id == organizationId, ct);
if (organization is null)
{
await SendNotFoundAsync(ct);
return;
}
if (!await organizationAccessService.CanAccessOrganizationAsync(User, organizationId, ct))
{
await SendForbiddenAsync(ct);
return;
}
IReadOnlyCollection<string> currentUserPermissions = await organizationAccessService.GetUserOrganizationPermissionsAsync(
User,
organizationId,
ct);
IReadOnlyCollection<OrganizationMemberDto> members = await GetMembersAsync(organizationId, ct);
IReadOnlyCollection<WorkspaceDto> workspaces = await GetWorkspacesAsync(organizationId, ct);
await SendOkAsync(
OrganizationDto.FromOrganization(
organization,
currentUserPermissions,
members,
workspaces),
ct);
}
private async Task<IReadOnlyCollection<OrganizationMemberDto>> GetMembersAsync(
Guid organizationId,
CancellationToken ct)
{
var rows = await dbContext.OrganizationMemberships
.Where(membership => membership.OrganizationId == organizationId)
.Join(
dbContext.Users,
membership => membership.UserId,
user => user.Id,
(membership, user) => new { Membership = membership, User = user })
.OrderBy(row => row.User.Lastname)
.ThenBy(row => row.User.Firstname)
.ThenBy(row => row.User.Email)
.ToListAsync(ct);
return rows
.Select(row => new OrganizationMemberDto(
row.User.Id,
BuildDisplayName(row.User),
row.User.Email ?? string.Empty,
row.User.PortraitUrl,
row.Membership.Role,
OrganizationPermissionRules.GetPermissionsForRole(row.Membership.Role),
row.Membership.CreatedAt))
.ToArray();
}
private async Task<IReadOnlyCollection<WorkspaceDto>> GetWorkspacesAsync(
Guid organizationId,
CancellationToken ct)
{
var workspaces = await dbContext.Workspaces
.Where(workspace => workspace.OrganizationId == organizationId)
.OrderBy(workspace => workspace.Name)
.ToListAsync(ct);
return workspaces
.Select(workspace => WorkspaceDto.FromWorkspace(workspace, []))
.ToArray();
}
private static string BuildDisplayName(User user)
{
if (!string.IsNullOrWhiteSpace(user.Alias))
{
return user.Alias;
}
string fullName = $"{user.Firstname} {user.Lastname}".Trim();
if (!string.IsNullOrWhiteSpace(fullName))
{
return fullName;
}
return user.Email ?? user.UserName ?? user.Id.ToString();
}
}

View File

@@ -0,0 +1,41 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Modules.Organizations.Data;
using Socialize.Api.Modules.Organizations.Services;
namespace Socialize.Api.Modules.Organizations.Handlers;
public class GetOrganizationsHandler(
AppDbContext dbContext,
OrganizationAccessService organizationAccessService)
: EndpointWithoutRequest<IReadOnlyCollection<OrganizationDto>>
{
public override void Configure()
{
Get("/api/organizations");
Options(o => o.WithTags("Organizations"));
}
public override async Task HandleAsync(CancellationToken ct)
{
IReadOnlyCollection<Guid> organizationIds = await organizationAccessService.GetAccessibleOrganizationIdsAsync(User, ct);
List<Organization> organizations = await dbContext.Organizations
.Where(organization => organizationIds.Contains(organization.Id))
.OrderBy(organization => organization.Name)
.ToListAsync(ct);
List<OrganizationDto> response = [];
foreach (Organization organization in organizations)
{
IReadOnlyCollection<string> permissions = await organizationAccessService.GetUserOrganizationPermissionsAsync(
User,
organization.Id,
ct);
response.Add(OrganizationDto.FromOrganization(organization, permissions));
}
await SendOkAsync(response, ct);
}
}

View File

@@ -0,0 +1,39 @@
using Socialize.Api.Modules.Organizations.Data;
using Socialize.Api.Modules.Workspaces.Handlers;
namespace Socialize.Api.Modules.Organizations.Handlers;
public record OrganizationMemberDto(
Guid UserId,
string DisplayName,
string Email,
string? PortraitUrl,
string Role,
IReadOnlyCollection<string> Permissions,
DateTimeOffset CreatedAt);
public record OrganizationDto(
Guid Id,
string Name,
Guid OwnerUserId,
IReadOnlyCollection<string> CurrentUserPermissions,
IReadOnlyCollection<OrganizationMemberDto> Members,
IReadOnlyCollection<WorkspaceDto> Workspaces,
DateTimeOffset CreatedAt)
{
public static OrganizationDto FromOrganization(
Organization organization,
IReadOnlyCollection<string> currentUserPermissions,
IReadOnlyCollection<OrganizationMemberDto>? members = null,
IReadOnlyCollection<WorkspaceDto>? workspaces = null)
{
return new OrganizationDto(
organization.Id,
organization.Name,
organization.OwnerUserId,
currentUserPermissions,
members ?? [],
workspaces ?? [],
organization.CreatedAt);
}
}

View File

@@ -0,0 +1,202 @@
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Identity.Contracts;
namespace Socialize.Api.Modules.Organizations.Services;
public sealed class OrganizationAccessService(
AppDbContext dbContext)
{
public bool IsGlobalManager(ClaimsPrincipal user)
{
return user.IsInRole(KnownRoles.Administrator) || user.IsInRole(KnownRoles.Manager);
}
public async Task<IReadOnlyCollection<Guid>> GetAccessibleOrganizationIdsAsync(
ClaimsPrincipal user,
CancellationToken ct)
{
if (IsGlobalManager(user))
{
return await dbContext.Organizations
.Select(organization => organization.Id)
.ToArrayAsync(ct);
}
Guid userId = user.GetUserId();
Guid[] ownedOrganizationIds = await dbContext.Organizations
.Where(organization => organization.OwnerUserId == userId)
.Select(organization => organization.Id)
.ToArrayAsync(ct);
Guid[] memberOrganizationIds = await dbContext.OrganizationMemberships
.Where(membership => membership.UserId == userId)
.Select(membership => membership.OrganizationId)
.ToArrayAsync(ct);
return ownedOrganizationIds
.Concat(memberOrganizationIds)
.Distinct()
.ToArray();
}
public async Task<IReadOnlyCollection<Guid>> GetAccessibleWorkspaceIdsAsync(
ClaimsPrincipal user,
CancellationToken ct)
{
if (IsGlobalManager(user))
{
return await dbContext.Workspaces
.Select(workspace => workspace.Id)
.ToArrayAsync(ct);
}
Guid[] directWorkspaceIds = user.GetWorkspaceScopeIds().ToArray();
Guid[] organizationWorkspaceIds = await GetInheritedWorkspaceIdsAsync(user, OrganizationPermissions.AccessOwnedWorkspaces, ct);
return directWorkspaceIds
.Concat(organizationWorkspaceIds)
.Distinct()
.ToArray();
}
public async Task<bool> CanAccessOrganizationAsync(
ClaimsPrincipal user,
Guid organizationId,
CancellationToken ct)
{
if (IsGlobalManager(user))
{
return true;
}
Guid userId = user.GetUserId();
return await dbContext.Organizations.AnyAsync(
organization => organization.Id == organizationId && organization.OwnerUserId == userId,
ct)
|| await dbContext.OrganizationMemberships.AnyAsync(
membership => membership.OrganizationId == organizationId && membership.UserId == userId,
ct);
}
public async Task<bool> HasOrganizationPermissionAsync(
ClaimsPrincipal user,
Guid organizationId,
string permission,
CancellationToken ct)
{
if (IsGlobalManager(user))
{
return true;
}
Guid userId = user.GetUserId();
bool owner = await dbContext.Organizations.AnyAsync(
organization => organization.Id == organizationId && organization.OwnerUserId == userId,
ct);
if (owner)
{
return OrganizationPermissionRules.RoleHasPermission(OrganizationRoles.Owner, permission);
}
string[] roles = await dbContext.OrganizationMemberships
.Where(membership => membership.OrganizationId == organizationId && membership.UserId == userId)
.Select(membership => membership.Role)
.ToArrayAsync(ct);
return roles.Any(role => OrganizationPermissionRules.RoleHasPermission(role, permission));
}
public async Task<IReadOnlyCollection<string>> GetUserOrganizationPermissionsAsync(
ClaimsPrincipal user,
Guid organizationId,
CancellationToken ct)
{
if (IsGlobalManager(user))
{
return OrganizationPermissionRules.GetPermissionsForRole(OrganizationRoles.Owner);
}
Guid userId = user.GetUserId();
bool owner = await dbContext.Organizations.AnyAsync(
organization => organization.Id == organizationId && organization.OwnerUserId == userId,
ct);
if (owner)
{
return OrganizationPermissionRules.GetPermissionsForRole(OrganizationRoles.Owner);
}
string[] roles = await dbContext.OrganizationMemberships
.Where(membership => membership.OrganizationId == organizationId && membership.UserId == userId)
.Select(membership => membership.Role)
.ToArrayAsync(ct);
return roles
.SelectMany(OrganizationPermissionRules.GetPermissionsForRole)
.Distinct(StringComparer.Ordinal)
.OrderBy(permission => permission)
.ToArray();
}
public async Task<bool> HasInheritedWorkspacePermissionAsync(
ClaimsPrincipal user,
Guid workspaceId,
string permission,
CancellationToken ct)
{
if (IsGlobalManager(user))
{
return true;
}
Guid? organizationId = await dbContext.Workspaces
.Where(workspace => workspace.Id == workspaceId)
.Select(workspace => (Guid?)workspace.OrganizationId)
.SingleOrDefaultAsync(ct);
return organizationId.HasValue &&
await HasOrganizationPermissionAsync(user, organizationId.Value, permission, ct);
}
private async Task<Guid[]> GetInheritedWorkspaceIdsAsync(
ClaimsPrincipal user,
string permission,
CancellationToken ct)
{
Guid userId = user.GetUserId();
Guid[] ownedOrganizationIds = await dbContext.Organizations
.Where(organization => organization.OwnerUserId == userId)
.Select(organization => organization.Id)
.ToArrayAsync(ct);
List<Data.OrganizationMembership> memberships = await dbContext.OrganizationMemberships
.Where(membership => membership.UserId == userId)
.ToListAsync(ct);
Guid[] memberOrganizationIds = memberships
.Where(membership => OrganizationPermissionRules.RoleHasPermission(membership.Role, permission))
.Select(membership => membership.OrganizationId)
.ToArray();
Guid[] organizationIds = ownedOrganizationIds
.Concat(memberOrganizationIds)
.Distinct()
.ToArray();
if (organizationIds.Length == 0)
{
return [];
}
return await dbContext.Workspaces
.Where(workspace => organizationIds.Contains(workspace.OrganizationId))
.Select(workspace => workspace.Id)
.ToArrayAsync(ct);
}
}

View File

@@ -0,0 +1,50 @@
namespace Socialize.Api.Modules.Organizations.Services;
public static class OrganizationPermissionRules
{
public static IReadOnlyCollection<string> GetPermissionsForRole(string role)
{
return role switch
{
OrganizationRoles.Owner =>
[
OrganizationPermissions.ManageOrganizationSettings,
OrganizationPermissions.ManageOrganizationMembers,
OrganizationPermissions.CreateWorkspaces,
OrganizationPermissions.ManageWorkspaces,
OrganizationPermissions.ManageBilling,
OrganizationPermissions.ManageConnectors,
OrganizationPermissions.AccessOwnedWorkspaces,
],
OrganizationRoles.Admin =>
[
OrganizationPermissions.ManageOrganizationSettings,
OrganizationPermissions.ManageOrganizationMembers,
OrganizationPermissions.CreateWorkspaces,
OrganizationPermissions.ManageWorkspaces,
OrganizationPermissions.ManageConnectors,
OrganizationPermissions.AccessOwnedWorkspaces,
],
OrganizationRoles.BillingManager =>
[
OrganizationPermissions.ManageBilling,
OrganizationPermissions.AccessOwnedWorkspaces,
],
OrganizationRoles.ConnectorManager =>
[
OrganizationPermissions.ManageConnectors,
OrganizationPermissions.AccessOwnedWorkspaces,
],
OrganizationRoles.Member =>
[
OrganizationPermissions.AccessOwnedWorkspaces,
],
_ => [],
};
}
public static bool RoleHasPermission(string role, string permission)
{
return GetPermissionsForRole(role).Contains(permission, StringComparer.Ordinal);
}
}

View File

@@ -0,0 +1,12 @@
namespace Socialize.Api.Modules.Organizations.Services;
public static class OrganizationPermissions
{
public const string ManageOrganizationSettings = "ManageOrganizationSettings";
public const string ManageOrganizationMembers = "ManageOrganizationMembers";
public const string CreateWorkspaces = "CreateWorkspaces";
public const string ManageWorkspaces = "ManageWorkspaces";
public const string ManageBilling = "ManageBilling";
public const string ManageConnectors = "ManageConnectors";
public const string AccessOwnedWorkspaces = "AccessOwnedWorkspaces";
}

View File

@@ -0,0 +1,10 @@
namespace Socialize.Api.Modules.Organizations.Services;
public static class OrganizationRoles
{
public const string Owner = "Owner";
public const string Admin = "Admin";
public const string BillingManager = "BillingManager";
public const string ConnectorManager = "ConnectorManager";
public const string Member = "Member";
}

View File

@@ -1,27 +0,0 @@
using Microsoft.EntityFrameworkCore;
namespace Socialize.Api.Modules.Projects.Data;
public static class ProjectModelConfiguration
{
public static ModelBuilder ConfigureProjectsModule(this ModelBuilder modelBuilder)
{
modelBuilder.Entity<Project>(project =>
{
project.ToTable("Projects");
project.HasKey(x => x.Id);
project.Property(x => x.Name).HasMaxLength(256).IsRequired();
project.Property(x => x.Description).HasMaxLength(4000);
project.Property(x => x.Notes).HasMaxLength(4000);
project.Property(x => x.Status).HasMaxLength(64).IsRequired();
project.Property(x => x.CreatedAt)
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
project.HasIndex(x => new { x.ClientId, x.Name }).IsUnique();
project.HasIndex(x => x.WorkspaceId);
project.HasIndex(x => x.ClientId);
});
return modelBuilder;
}
}

View File

@@ -1,12 +0,0 @@
using Socialize.Api.Modules.Projects.Data;
namespace Socialize.Api.Modules.Projects;
public static class DependencyInjection
{
public static WebApplicationBuilder AddProjectsModule(
this WebApplicationBuilder builder)
{
return builder;
}
}

View File

@@ -1,89 +0,0 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Data;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Projects.Data;
namespace Socialize.Api.Modules.Projects.Handlers;
public record GetProjectsRequest(Guid? WorkspaceId, Guid? ClientId);
public record ProjectDto(
Guid Id,
Guid WorkspaceId,
Guid ClientId,
string Name,
string? Description,
string? Notes,
string Status,
DateTimeOffset StartDate,
DateTimeOffset EndDate);
public class GetProjectsHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService)
: Endpoint<GetProjectsRequest, IReadOnlyCollection<ProjectDto>>
{
public override void Configure()
{
Get("/api/projects");
Options(o => o.WithTags("Projects"));
}
public override async Task HandleAsync(GetProjectsRequest request, CancellationToken ct)
{
IQueryable<Project> query = dbContext.Projects.AsQueryable();
if (accessScopeService.IsManager(User))
{
if (request.WorkspaceId.HasValue)
{
query = query.Where(project => project.WorkspaceId == request.WorkspaceId.Value);
}
}
else
{
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
IReadOnlyCollection<Guid> projectScopeIds = User.GetProjectScopeIds();
query = query.Where(project => workspaceScopeIds.Contains(project.WorkspaceId));
if (clientScopeIds.Count > 0)
{
query = query.Where(project => clientScopeIds.Contains(project.ClientId));
}
if (projectScopeIds.Count > 0)
{
query = query.Where(project => projectScopeIds.Contains(project.Id));
}
}
if (request.ClientId.HasValue)
{
query = query.Where(project => project.ClientId == request.ClientId.Value);
}
if (request.WorkspaceId.HasValue)
{
query = query.Where(project => project.WorkspaceId == request.WorkspaceId.Value);
}
List<ProjectDto> projects = await query
.OrderBy(project => project.Name)
.Select(project => new ProjectDto(
project.Id,
project.WorkspaceId,
project.ClientId,
project.Name,
project.Description,
project.Notes,
project.Status,
project.StartDate,
project.EndDate))
.ToListAsync(ct);
await SendOkAsync(projects, ct);
}
}

View File

@@ -6,6 +6,7 @@ public class Workspace
public required string Name { get; set; }
public required string Slug { get; set; }
public string? LogoUrl { get; set; }
public Guid OrganizationId { get; set; }
public Guid OwnerUserId { get; set; }
public required string TimeZone { get; set; }
public string ApprovalMode { get; set; } = "Required";

View File

@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
using Socialize.Api.Modules.Organizations.Data;
namespace Socialize.Api.Modules.Workspaces.Data;
@@ -22,7 +23,12 @@ public static class WorkspaceModelConfiguration
.ValueGeneratedOnAdd()
.HasDefaultValueSql("CURRENT_TIMESTAMP");
workspace.HasIndex(x => x.Slug).IsUnique();
workspace.HasIndex(x => x.OrganizationId);
workspace.HasIndex(x => x.OwnerUserId);
workspace.HasOne<Organization>()
.WithMany()
.HasForeignKey(x => x.OrganizationId)
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity<WorkspaceInvite>(workspaceInvite =>

View File

@@ -47,7 +47,7 @@ public class ChangeWorkspaceLogoHandler(
return;
}
if (!accessScopeService.CanManageWorkspace(User, workspace.Id))
if (!await accessScopeService.CanManageWorkspaceAsync(User, workspace.Id, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -7,6 +7,7 @@ using Socialize.Api.Modules.Workspaces.Data;
namespace Socialize.Api.Modules.Workspaces.Handlers;
public record CreateWorkspaceRequest(
Guid OrganizationId,
string Name,
string Slug,
string TimeZone);
@@ -16,6 +17,7 @@ public class CreateWorkspaceRequestValidator
{
public CreateWorkspaceRequestValidator()
{
RuleFor(x => x.OrganizationId).NotEmpty();
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
RuleFor(x => x.Slug)
.NotEmpty()
@@ -38,12 +40,21 @@ public class CreateWorkspaceHandler(
public override async Task HandleAsync(CreateWorkspaceRequest request, CancellationToken ct)
{
if (!accessScopeService.IsManager(User))
if (!await accessScopeService.CanCreateWorkspaceAsync(User, request.OrganizationId, ct))
{
await SendForbiddenAsync(ct);
return;
}
bool organizationExists = await dbContext.Organizations
.AnyAsync(organization => organization.Id == request.OrganizationId, ct);
if (!organizationExists)
{
AddError(request => request.OrganizationId, "The selected organization does not exist.");
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
return;
}
string normalizedName = request.Name.Trim();
string normalizedSlug = request.Slug.Trim().ToLowerInvariant();
string normalizedTimeZone = request.TimeZone.Trim();
@@ -61,6 +72,7 @@ public class CreateWorkspaceHandler(
Workspace workspace = new()
{
Id = Guid.NewGuid(),
OrganizationId = request.OrganizationId,
Name = normalizedName,
Slug = normalizedSlug,
OwnerUserId = User.GetUserId(),
@@ -71,18 +83,7 @@ public class CreateWorkspaceHandler(
dbContext.Workspaces.Add(workspace);
await dbContext.SaveChangesAsync(ct);
WorkspaceDto dto = new(
workspace.Id,
workspace.Name,
workspace.Slug,
workspace.LogoUrl,
workspace.TimeZone,
workspace.ApprovalMode,
workspace.SchedulePostsAutomaticallyOnApproval,
workspace.LockContentAfterApproval,
workspace.SendAutomaticApprovalReminders,
[],
workspace.CreatedAt);
WorkspaceDto dto = WorkspaceDto.FromWorkspace(workspace, []);
await SendAsync(dto, StatusCodes.Status201Created, ct);
}

View File

@@ -43,7 +43,7 @@ public class CreateWorkspaceInviteHandler(
{
Guid workspaceId = Route<Guid>("workspaceId");
if (!accessScopeService.CanManageWorkspace(User, workspaceId))
if (!await accessScopeService.CanManageWorkspaceAsync(User, workspaceId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -29,7 +29,7 @@ public class GetWorkspaceInvitesHandler(
{
Guid workspaceId = Route<Guid>("workspaceId");
if (!accessScopeService.CanManageWorkspace(User, workspaceId))
if (!await accessScopeService.CanManageWorkspaceAsync(User, workspaceId, ct))
{
await SendForbiddenAsync(ct);
return;

View File

@@ -4,6 +4,7 @@ using System.Security.Claims;
using Socialize.Api.Data;
using Socialize.Api.Modules.Identity.Data;
using Socialize.Api.Infrastructure.Security;
using Socialize.Api.Modules.Workspaces.Data;
namespace Socialize.Api.Modules.Workspaces.Handlers;
@@ -12,6 +13,7 @@ public record WorkspaceMemberDto(
string DisplayName,
string Email,
string? PortraitUrl,
string RelationshipCategory,
IReadOnlyCollection<string> Roles);
public class GetWorkspaceMembersHandler(
@@ -29,12 +31,20 @@ public class GetWorkspaceMembersHandler(
{
Guid workspaceId = Route<Guid>("workspaceId");
if (!accessScopeService.CanManageWorkspace(User, workspaceId))
if (!await accessScopeService.CanManageWorkspaceAsync(User, workspaceId, ct))
{
await SendForbiddenAsync(ct);
return;
}
Workspace? workspace = await dbContext.Workspaces
.SingleOrDefaultAsync(candidate => candidate.Id == workspaceId, ct);
if (workspace is null)
{
await SendNotFoundAsync(ct);
return;
}
string workspaceClaimValue = workspaceId.ToString();
var users = await dbContext.Users
@@ -42,7 +52,11 @@ public class GetWorkspaceMembersHandler(
dbContext.UserClaims.Any(claim =>
claim.UserId == candidate.Id &&
claim.ClaimType == KnownClaims.WorkspaceScope &&
claim.ClaimValue == workspaceClaimValue))
claim.ClaimValue == workspaceClaimValue) ||
dbContext.OrganizationMemberships.Any(membership =>
membership.UserId == candidate.Id &&
membership.OrganizationId == workspace.OrganizationId) ||
candidate.Id == workspace.OwnerUserId)
.OrderBy(candidate => candidate.Lastname)
.ThenBy(candidate => candidate.Firstname)
.ThenBy(candidate => candidate.Email)
@@ -70,12 +84,19 @@ public class GetWorkspaceMembersHandler(
.ToArray(),
ct);
HashSet<Guid> organizationMemberUserIds = await dbContext.OrganizationMemberships
.Where(membership => membership.OrganizationId == workspace.OrganizationId)
.Select(membership => membership.UserId)
.ToHashSetAsync(ct);
organizationMemberUserIds.Add(workspace.OwnerUserId);
var members = users
.Select(candidate => new WorkspaceMemberDto(
candidate.Id,
BuildDisplayName(candidate),
candidate.Email ?? string.Empty,
candidate.PortraitUrl,
organizationMemberUserIds.Contains(candidate.Id) ? "Organization Member" : "External Collaborator",
rolesByUserId.GetValueOrDefault(candidate.Id) ?? Array.Empty<string>()))
.ToList();

View File

@@ -19,6 +19,7 @@ public record ApprovalStepConfigurationDto(
public record WorkspaceDto(
Guid Id,
Guid OrganizationId,
string Name,
string Slug,
string? LogoUrl,
@@ -28,7 +29,27 @@ public record WorkspaceDto(
bool LockContentAfterApproval,
bool SendAutomaticApprovalReminders,
IReadOnlyCollection<ApprovalStepConfigurationDto> ApprovalSteps,
DateTimeOffset CreatedAt);
DateTimeOffset CreatedAt)
{
public static WorkspaceDto FromWorkspace(
Workspace workspace,
IReadOnlyCollection<ApprovalStepConfigurationDto> approvalSteps)
{
return new WorkspaceDto(
workspace.Id,
workspace.OrganizationId,
workspace.Name,
workspace.Slug,
workspace.LogoUrl,
workspace.TimeZone,
workspace.ApprovalMode,
workspace.SchedulePostsAutomaticallyOnApproval,
workspace.LockContentAfterApproval,
workspace.SendAutomaticApprovalReminders,
approvalSteps,
workspace.CreatedAt);
}
}
internal class GetWorkspacesHandler(
AppDbContext dbContext,
@@ -43,13 +64,9 @@ internal class GetWorkspacesHandler(
public override async Task HandleAsync(CancellationToken ct)
{
var query = dbContext.Workspaces.AsQueryable();
if (!accessScopeService.IsManager(User))
{
var workspaceScopeIds = User.GetWorkspaceScopeIds();
query = query.Where(workspace => workspaceScopeIds.Contains(workspace.Id));
}
IReadOnlyCollection<Guid> accessibleWorkspaceIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
var query = dbContext.Workspaces
.Where(workspace => accessibleWorkspaceIds.Contains(workspace.Id));
var workspaceRows = await query
.OrderBy(workspace => workspace.Name)
@@ -71,18 +88,9 @@ internal class GetWorkspacesHandler(
.ToArray());
var workspaces = workspaceRows
.Select(workspace => new WorkspaceDto(
workspace.Id,
workspace.Name,
workspace.Slug,
workspace.LogoUrl,
workspace.TimeZone,
workspace.ApprovalMode,
workspace.SchedulePostsAutomaticallyOnApproval,
workspace.LockContentAfterApproval,
workspace.SendAutomaticApprovalReminders,
approvalStepsByWorkspaceId.GetValueOrDefault(workspace.Id) ?? Array.Empty<ApprovalStepConfigurationDto>(),
workspace.CreatedAt))
.Select(workspace => WorkspaceDto.FromWorkspace(
workspace,
approvalStepsByWorkspaceId.GetValueOrDefault(workspace.Id) ?? Array.Empty<ApprovalStepConfigurationDto>()))
.ToList();
await SendOkAsync(workspaces, ct);

View File

@@ -73,7 +73,7 @@ public class UpdateWorkspaceHandler(
return;
}
if (!accessScopeService.CanManageWorkspace(User, workspace.Id))
if (!await accessScopeService.CanManageWorkspaceAsync(User, workspace.Id, ct))
{
await SendForbiddenAsync(ct);
return;
@@ -154,18 +154,7 @@ public class UpdateWorkspaceHandler(
step.CreatedAt))
.ToListAsync(ct);
WorkspaceDto dto = new(
workspace.Id,
workspace.Name,
workspace.Slug,
workspace.LogoUrl,
workspace.TimeZone,
workspace.ApprovalMode,
workspace.SchedulePostsAutomaticallyOnApproval,
workspace.LockContentAfterApproval,
workspace.SendAutomaticApprovalReminders,
approvalSteps,
workspace.CreatedAt);
WorkspaceDto dto = WorkspaceDto.FromWorkspace(workspace, approvalSteps);
await SendOkAsync(dto, ct);
}

View File

@@ -16,7 +16,8 @@ using Socialize.Api.Modules.ContentItems;
using Socialize.Api.Modules.Feedback;
using Socialize.Api.Modules.Identity;
using Socialize.Api.Modules.Notifications;
using Socialize.Api.Modules.Projects;
using Socialize.Api.Modules.Campaigns;
using Socialize.Api.Modules.Organizations;
using Socialize.Api.Modules.Workspaces;
@@ -62,9 +63,10 @@ var postgresConnectionString = builder.Configuration.GetConnectionString("Postgr
builder.Services.AddAppData(postgresConnectionString);
builder.AddInfrastructureModule();
builder.AddIdentityModule();
builder.AddOrganizationsModule();
builder.AddWorkspaceModule();
builder.AddClientsModule();
builder.AddProjectsModule();
builder.AddCampaignsModule();
builder.AddContentItemsModule();
builder.AddAssetsModule();
builder.AddCommentsModule();

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Framework" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Utilities.Core" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Tasks.Core" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.IO.Redist" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.1" newVersion="6.0.0.1"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -0,0 +1,260 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v6.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v6.0": {
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/4.14.0-3.25262.10": {
"dependencies": {
"Microsoft.Build.Locator": "1.6.10",
"Microsoft.CodeAnalysis.NetAnalyzers": "8.0.0-preview.23468.1",
"Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers": "3.3.4-beta1.22504.1",
"Microsoft.DotNet.XliffTasks": "9.0.0-beta.25255.5",
"Microsoft.VisualStudio.Threading.Analyzers": "17.13.2",
"Newtonsoft.Json": "13.0.3",
"Roslyn.Diagnostics.Analyzers": "3.11.0-beta1.24081.1",
"System.Collections.Immutable": "9.0.0",
"System.CommandLine": "2.0.0-beta4.24528.1"
},
"runtime": {
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll": {}
},
"resources": {
"cs/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "cs"
},
"de/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "de"
},
"es/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "es"
},
"fr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "fr"
},
"it/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "it"
},
"ja/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "ja"
},
"ko/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "ko"
},
"pl/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "pl"
},
"pt-BR/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "pt-BR"
},
"ru/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "ru"
},
"tr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "tr"
},
"zh-Hans/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "zh-Hans"
},
"zh-Hant/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.Build.Locator/1.6.10": {
"runtime": {
"lib/net6.0/Microsoft.Build.Locator.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.6.10.57384"
}
}
},
"Microsoft.CodeAnalysis.BannedApiAnalyzers/3.11.0-beta1.24081.1": {},
"Microsoft.CodeAnalysis.NetAnalyzers/8.0.0-preview.23468.1": {},
"Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers/3.3.4-beta1.22504.1": {},
"Microsoft.CodeAnalysis.PublicApiAnalyzers/3.11.0-beta1.24081.1": {},
"Microsoft.DotNet.XliffTasks/9.0.0-beta.25255.5": {},
"Microsoft.VisualStudio.Threading.Analyzers/17.13.2": {},
"Newtonsoft.Json/13.0.3": {
"runtime": {
"lib/net6.0/Newtonsoft.Json.dll": {
"assemblyVersion": "13.0.0.0",
"fileVersion": "13.0.3.27908"
}
}
},
"Roslyn.Diagnostics.Analyzers/3.11.0-beta1.24081.1": {
"dependencies": {
"Microsoft.CodeAnalysis.BannedApiAnalyzers": "3.11.0-beta1.24081.1",
"Microsoft.CodeAnalysis.PublicApiAnalyzers": "3.11.0-beta1.24081.1"
}
},
"System.Collections.Immutable/9.0.0": {
"dependencies": {
"System.Memory": "4.5.5",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
},
"runtime": {
"lib/netstandard2.0/System.Collections.Immutable.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.24.52809"
}
}
},
"System.CommandLine/2.0.0-beta4.24528.1": {
"dependencies": {
"System.Memory": "4.5.5"
},
"runtime": {
"lib/netstandard2.0/System.CommandLine.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.24.52801"
}
},
"resources": {
"lib/netstandard2.0/cs/System.CommandLine.resources.dll": {
"locale": "cs"
},
"lib/netstandard2.0/de/System.CommandLine.resources.dll": {
"locale": "de"
},
"lib/netstandard2.0/es/System.CommandLine.resources.dll": {
"locale": "es"
},
"lib/netstandard2.0/fr/System.CommandLine.resources.dll": {
"locale": "fr"
},
"lib/netstandard2.0/it/System.CommandLine.resources.dll": {
"locale": "it"
},
"lib/netstandard2.0/ja/System.CommandLine.resources.dll": {
"locale": "ja"
},
"lib/netstandard2.0/ko/System.CommandLine.resources.dll": {
"locale": "ko"
},
"lib/netstandard2.0/pl/System.CommandLine.resources.dll": {
"locale": "pl"
},
"lib/netstandard2.0/pt-BR/System.CommandLine.resources.dll": {
"locale": "pt-BR"
},
"lib/netstandard2.0/ru/System.CommandLine.resources.dll": {
"locale": "ru"
},
"lib/netstandard2.0/tr/System.CommandLine.resources.dll": {
"locale": "tr"
},
"lib/netstandard2.0/zh-Hans/System.CommandLine.resources.dll": {
"locale": "zh-Hans"
},
"lib/netstandard2.0/zh-Hant/System.CommandLine.resources.dll": {
"locale": "zh-Hant"
}
}
},
"System.Memory/4.5.5": {},
"System.Runtime.CompilerServices.Unsafe/6.0.0": {}
}
},
"libraries": {
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/4.14.0-3.25262.10": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Microsoft.Build.Locator/1.6.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-DJhCkTGqy1LMJzEmG/2qxRTMHwdPc3WdVoGQI5o5mKHVo4dsHrCMLIyruwU/NSvPNSdvONlaf7jdFXnAMuxAuA==",
"path": "microsoft.build.locator/1.6.10",
"hashPath": "microsoft.build.locator.1.6.10.nupkg.sha512"
},
"Microsoft.CodeAnalysis.BannedApiAnalyzers/3.11.0-beta1.24081.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-DH6L3rsbjppLrHM2l2/NKbnMaYd0NFHx2pjZaFdrVcRkONrV3i9FHv6Id8Dp6/TmjhXQsJVJJFbhhjkpuP1xxg==",
"path": "microsoft.codeanalysis.bannedapianalyzers/3.11.0-beta1.24081.1",
"hashPath": "microsoft.codeanalysis.bannedapianalyzers.3.11.0-beta1.24081.1.nupkg.sha512"
},
"Microsoft.CodeAnalysis.NetAnalyzers/8.0.0-preview.23468.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ZhIvyxmUCqb8OiU/VQfxfuAmIB4lQsjqhMVYKeoyxzSI+d7uR5Pzx3ZKoaIhPizQ15wa4lnyD6wg3TnSJ6P4LA==",
"path": "microsoft.codeanalysis.netanalyzers/8.0.0-preview.23468.1",
"hashPath": "microsoft.codeanalysis.netanalyzers.8.0.0-preview.23468.1.nupkg.sha512"
},
"Microsoft.CodeAnalysis.PerformanceSensitiveAnalyzers/3.3.4-beta1.22504.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-2XRlqPAzVke7Sb80+UqaC7o57OwfK+tIr+aIOxrx41RWDMeR2SBUW7kL4sd6hfLFfBNsLo3W5PT+UwfvwPaOzA==",
"path": "microsoft.codeanalysis.performancesensitiveanalyzers/3.3.4-beta1.22504.1",
"hashPath": "microsoft.codeanalysis.performancesensitiveanalyzers.3.3.4-beta1.22504.1.nupkg.sha512"
},
"Microsoft.CodeAnalysis.PublicApiAnalyzers/3.11.0-beta1.24081.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3bYGBihvoNO0rhCOG1U9O50/4Q8suZ+glHqQLIAcKvnodSnSW+dYWYzTNb1UbS8pUS8nAUfxSFMwuMup/G5DtQ==",
"path": "microsoft.codeanalysis.publicapianalyzers/3.11.0-beta1.24081.1",
"hashPath": "microsoft.codeanalysis.publicapianalyzers.3.11.0-beta1.24081.1.nupkg.sha512"
},
"Microsoft.DotNet.XliffTasks/9.0.0-beta.25255.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-bb0fZB5ViPscdfYeWlmtyXJMzNkgcpkV5RWmXktfV9lwIUZgNZmFotUXrdcTyZzrN7v1tQK/Y6BGnbkP9gEsXg==",
"path": "microsoft.dotnet.xlifftasks/9.0.0-beta.25255.5",
"hashPath": "microsoft.dotnet.xlifftasks.9.0.0-beta.25255.5.nupkg.sha512"
},
"Microsoft.VisualStudio.Threading.Analyzers/17.13.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Qcd8IlaTXZVq3wolBnzby1P7kWihdWaExtD8riumiKuG1sHa8EgjV/o70TMjTaeUMhomBbhfdC9OPwAHoZfnjQ==",
"path": "microsoft.visualstudio.threading.analyzers/17.13.2",
"hashPath": "microsoft.visualstudio.threading.analyzers.17.13.2.nupkg.sha512"
},
"Newtonsoft.Json/13.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
"path": "newtonsoft.json/13.0.3",
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
},
"Roslyn.Diagnostics.Analyzers/3.11.0-beta1.24081.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-reHqZCDKifA+DURcL8jUfYkMGL4FpgNt5LI0uWTS6IpM8kKVbu/kO8byZsqfhBu4wUzT3MBDcoMfzhZPdENIpg==",
"path": "roslyn.diagnostics.analyzers/3.11.0-beta1.24081.1",
"hashPath": "roslyn.diagnostics.analyzers.3.11.0-beta1.24081.1.nupkg.sha512"
},
"System.Collections.Immutable/9.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==",
"path": "system.collections.immutable/9.0.0",
"hashPath": "system.collections.immutable.9.0.0.nupkg.sha512"
},
"System.CommandLine/2.0.0-beta4.24528.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Xt8tsSU8yd0ZpbT9gl5DAwkMYWLo8PV1fq2R/belrUbHVVOIKqhLfbWksbdknUDpmzMHZenBtD6AGAp9uJTa2w==",
"path": "system.commandline/2.0.0-beta4.24528.1",
"hashPath": "system.commandline.2.0.0-beta4.24528.1.nupkg.sha512"
},
"System.Memory/4.5.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
"path": "system.memory/4.5.5",
"hashPath": "system.memory.4.5.5.nupkg.sha512"
},
"System.Runtime.CompilerServices.Unsafe/6.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
"path": "system.runtime.compilerservices.unsafe/6.0.0",
"hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
}
}
}

View File

@@ -0,0 +1,659 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Framework" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Utilities.Core" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Tasks.Core" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.VisualBasic.Core" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-11.0.0.0" newVersion="11.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Win32.Primitives" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Win32.Registry" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Collections.Concurrent" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Collections.NonGeneric" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Collections.Specialized" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Collections" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.ComponentModel.Annotations" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.ComponentModel.EventBasedAsync" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.ComponentModel.Primitives" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.ComponentModel.TypeConverter" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.ComponentModel" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Console" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Data.Common" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.Contracts" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.FileVersionInfo" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.Process" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.StackTrace" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.TextWriterTraceListener" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.TraceSource" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Diagnostics.Tracing" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Drawing.Primitives" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.IO.Compression.ZipFile" publicKeyToken="b77a5c561934e089"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.IO.Compression" publicKeyToken="b77a5c561934e089" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.IO.FileSystem.AccessControl" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.IO.FileSystem.DriveInfo" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.IO.FileSystem.Watcher" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.IO.IsolatedStorage" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.IO.MemoryMappedFiles" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.IO.Pipes.AccessControl" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.IO.Pipes" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Linq.Expressions" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Linq.Parallel" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Linq.Queryable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Linq" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.HttpListener" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.Mail" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.NameResolution" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.NetworkInformation" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.Ping" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.Primitives" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.Requests" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.Security" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.ServicePoint" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.Sockets" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.WebClient" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.WebHeaderCollection" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.WebProxy" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.WebSockets.Client" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Net.WebSockets" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.ObjectModel" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Reflection.Emit.ILGeneration" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Reflection.Emit.Lightweight" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Reflection.Emit" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Reflection.Primitives" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Resources.Writer" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.VisualC" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.InteropServices.RuntimeInformation"
publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.InteropServices" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.Numerics" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.Serialization.Formatters" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.Serialization.Json" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.Serialization.Primitives" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.Serialization.Xml" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Security.AccessControl" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Security.Claims" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Security.Cryptography.Algorithms" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Security.Cryptography.Cng" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Security.Cryptography.Csp" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Security.Cryptography.Encoding" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Security.Cryptography.Primitives" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Security.Cryptography.X509Certificates" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Security.Principal.Windows" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Text.Encoding.Extensions" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Text.RegularExpressions" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading.Overlapped" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks.Parallel" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading.Thread" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading.ThreadPool" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Transactions.Local" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Web.HttpUtility" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Xml.ReaderWriter" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Xml.XDocument" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Xml.XPath.XDocument" publicKeyToken="b03f5f7f11d50a3a"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Xml.XPath" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Xml.XmlSerializer" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="netstandard" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Configuration.ConfigurationManager" publicKeyToken="cc7b13ffcd2ddd51"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Security.Cryptography.Xml" publicKeyToken="cc7b13ffcd2ddd51"
culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.CodeDom" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

Some files were not shown because too many files have changed in this diff Show More