feat: add organization domain foundation
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user