feat: add google drive dam foundation
This commit is contained in:
@@ -15,6 +15,7 @@ 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 Socialize.Api.Modules.Workspaces.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Socialize.Api.Infrastructure.TestData;
|
||||
@@ -261,6 +262,10 @@ internal static class TestDataSeedExtensions
|
||||
}
|
||||
|
||||
organization.Name = "Northstar Agency";
|
||||
organization.IsGoogleDriveDamEnabled = true;
|
||||
organization.GoogleDriveRootFolderId = "dev-socialize-dam-root";
|
||||
organization.GoogleDriveRootFolderName = "Socialize DAM";
|
||||
organization.GoogleDriveRootFolderUrl = "https://drive.google.com/drive/folders/dev-socialize-dam-root";
|
||||
organization.MembershipTierId = OrganizationMembershipTierSeed.AgencyId;
|
||||
organization.OwnerUserId = managerUserId;
|
||||
|
||||
@@ -465,6 +470,7 @@ internal static class TestDataSeedExtensions
|
||||
asset.DisplayName = "Spring launch cut";
|
||||
asset.GoogleDriveFileId = "dev-socialize-demo";
|
||||
asset.GoogleDriveLink = "https://drive.google.com/file/d/dev-socialize-demo/view";
|
||||
asset.GoogleDriveWorkspaceFolderPath = "Socialize DAM/luma-coffee";
|
||||
asset.PreviewUrl = "https://drive.google.com/thumbnail?id=dev-socialize-demo";
|
||||
asset.CurrentRevisionNumber = 2;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
@@ -587,6 +593,7 @@ internal static class TestDataSeedExtensions
|
||||
{
|
||||
Id = id,
|
||||
Name = string.Empty,
|
||||
Slug = string.Empty,
|
||||
TimeZone = string.Empty,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
@@ -594,6 +601,12 @@ internal static class TestDataSeedExtensions
|
||||
}
|
||||
|
||||
workspace.Name = name;
|
||||
workspace.Slug = await WorkspaceSlugGenerator.CreateUniqueAsync(
|
||||
dbContext,
|
||||
organizationId,
|
||||
string.IsNullOrWhiteSpace(workspace.Slug) ? name : workspace.Slug,
|
||||
workspace.Id,
|
||||
cancellationToken);
|
||||
workspace.OrganizationId = organizationId;
|
||||
workspace.OwnerUserId = ownerUserId;
|
||||
workspace.TimeZone = timeZone;
|
||||
|
||||
2657
backend/src/Socialize.Api/Migrations/20260508152102_AddGoogleDriveDamFoundation.Designer.cs
generated
Normal file
2657
backend/src/Socialize.Api/Migrations/20260508152102_AddGoogleDriveDamFoundation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
internal partial class AddGoogleDriveDamFoundation : Migration
|
||||
{
|
||||
private static readonly string[] WorkspaceOrganizationSlugIndexColumns =
|
||||
[
|
||||
"OrganizationId",
|
||||
"Slug",
|
||||
];
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Slug",
|
||||
table: "Workspaces",
|
||||
type: "character varying(96)",
|
||||
maxLength: 96,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
WITH normalized AS (
|
||||
SELECT
|
||||
"Id",
|
||||
"OrganizationId",
|
||||
COALESCE(
|
||||
NULLIF(
|
||||
trim(both '-' from lower(regexp_replace(trim("Name"), '[^a-zA-Z0-9]+', '-', 'g'))),
|
||||
''
|
||||
),
|
||||
'workspace'
|
||||
) AS "BaseSlug"
|
||||
FROM "Workspaces"
|
||||
),
|
||||
numbered AS (
|
||||
SELECT
|
||||
"Id",
|
||||
"BaseSlug",
|
||||
row_number() OVER (PARTITION BY "OrganizationId", "BaseSlug" ORDER BY "CreatedAt", "Id") AS "SlugIndex"
|
||||
FROM normalized
|
||||
)
|
||||
UPDATE "Workspaces"
|
||||
SET "Slug" = left(
|
||||
CASE
|
||||
WHEN numbered."SlugIndex" = 1 THEN numbered."BaseSlug"
|
||||
ELSE numbered."BaseSlug" || '-' || numbered."SlugIndex"
|
||||
END,
|
||||
96
|
||||
)
|
||||
FROM numbered
|
||||
WHERE "Workspaces"."Id" = numbered."Id";
|
||||
""");
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "GoogleDriveRootFolderId",
|
||||
table: "Organizations",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "GoogleDriveRootFolderName",
|
||||
table: "Organizations",
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "GoogleDriveRootFolderUrl",
|
||||
table: "Organizations",
|
||||
type: "character varying(2048)",
|
||||
maxLength: 2048,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsGoogleDriveDamEnabled",
|
||||
table: "Organizations",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "GoogleDriveWorkspaceFolderPath",
|
||||
table: "Assets",
|
||||
type: "character varying(512)",
|
||||
maxLength: 512,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Workspaces_OrganizationId_Slug",
|
||||
table: "Workspaces",
|
||||
columns: WorkspaceOrganizationSlugIndexColumns,
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Workspaces_OrganizationId_Slug",
|
||||
table: "Workspaces");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Slug",
|
||||
table: "Workspaces");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GoogleDriveRootFolderId",
|
||||
table: "Organizations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GoogleDriveRootFolderName",
|
||||
table: "Organizations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GoogleDriveRootFolderUrl",
|
||||
table: "Organizations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsGoogleDriveDamEnabled",
|
||||
table: "Organizations");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "GoogleDriveWorkspaceFolderPath",
|
||||
table: "Assets");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,6 +374,10 @@ namespace Socialize.Api.Migrations
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("GoogleDriveWorkspaceFolderPath")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("PreviewUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
@@ -1658,6 +1662,23 @@ namespace Socialize.Api.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("GoogleDriveRootFolderId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("GoogleDriveRootFolderName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("GoogleDriveRootFolderUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<bool>("IsGoogleDriveDamEnabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("LogoUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
@@ -2167,6 +2188,11 @@ namespace Socialize.Api.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(96)
|
||||
.HasColumnType("character varying(96)");
|
||||
|
||||
b.Property<string>("TimeZone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
@@ -2178,6 +2204,9 @@ namespace Socialize.Api.Migrations
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.HasIndex("OrganizationId", "Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Workspaces", (string)null);
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ internal class Asset
|
||||
public required string DisplayName { get; set; }
|
||||
public string? GoogleDriveFileId { get; set; }
|
||||
public string? GoogleDriveLink { get; set; }
|
||||
public string? GoogleDriveWorkspaceFolderPath { get; set; }
|
||||
public string? PreviewUrl { get; set; }
|
||||
public int CurrentRevisionNumber { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
@@ -17,6 +17,7 @@ internal static class AssetModelConfiguration
|
||||
asset.Property(x => x.DisplayName).HasMaxLength(256).IsRequired();
|
||||
asset.Property(x => x.GoogleDriveFileId).HasMaxLength(256);
|
||||
asset.Property(x => x.GoogleDriveLink).HasMaxLength(2048);
|
||||
asset.Property(x => x.GoogleDriveWorkspaceFolderPath).HasMaxLength(512);
|
||||
asset.Property(x => x.PreviewUrl).HasMaxLength(2048);
|
||||
asset.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
|
||||
@@ -6,6 +6,8 @@ using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using Socialize.Api.Modules.Organizations.Data;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||
@@ -67,6 +69,26 @@ internal class CreateGoogleDriveAssetHandler(
|
||||
return;
|
||||
}
|
||||
|
||||
Workspace? workspace = await dbContext.Workspaces
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == contentItem.WorkspaceId, ct);
|
||||
if (workspace is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
Organization? organization = await dbContext.Organizations
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == workspace.OrganizationId, ct);
|
||||
if (organization is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
string? workspaceFolderPath = organization.IsGoogleDriveDamEnabled
|
||||
? $"{organization.GoogleDriveRootFolderName}/{workspace.Slug}"
|
||||
: null;
|
||||
|
||||
Asset asset = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
@@ -77,6 +99,7 @@ internal class CreateGoogleDriveAssetHandler(
|
||||
DisplayName = request.DisplayName.Trim(),
|
||||
GoogleDriveFileId = request.GoogleDriveFileId.Trim(),
|
||||
GoogleDriveLink = request.GoogleDriveLink.Trim(),
|
||||
GoogleDriveWorkspaceFolderPath = workspaceFolderPath,
|
||||
PreviewUrl = string.IsNullOrWhiteSpace(request.PreviewUrl) ? null : request.PreviewUrl.Trim(),
|
||||
CurrentRevisionNumber = 1,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
@@ -111,6 +134,7 @@ internal class CreateGoogleDriveAssetHandler(
|
||||
assetType = asset.AssetType,
|
||||
sourceType = asset.SourceType,
|
||||
googleDriveFileId = asset.GoogleDriveFileId,
|
||||
googleDriveWorkspaceFolderPath = asset.GoogleDriveWorkspaceFolderPath,
|
||||
currentRevisionNumber = asset.CurrentRevisionNumber,
|
||||
})),
|
||||
ct);
|
||||
@@ -137,6 +161,7 @@ internal class CreateGoogleDriveAssetHandler(
|
||||
asset.DisplayName,
|
||||
asset.GoogleDriveFileId,
|
||||
asset.GoogleDriveLink,
|
||||
asset.GoogleDriveWorkspaceFolderPath,
|
||||
asset.PreviewUrl,
|
||||
asset.CurrentRevisionNumber,
|
||||
asset.CreatedAt,
|
||||
|
||||
@@ -26,6 +26,7 @@ internal record AssetDto(
|
||||
string DisplayName,
|
||||
string? GoogleDriveFileId,
|
||||
string? GoogleDriveLink,
|
||||
string? GoogleDriveWorkspaceFolderPath,
|
||||
string? PreviewUrl,
|
||||
int CurrentRevisionNumber,
|
||||
DateTimeOffset CreatedAt,
|
||||
@@ -70,6 +71,7 @@ internal class GetAssetsHandler(
|
||||
asset.DisplayName,
|
||||
asset.GoogleDriveFileId,
|
||||
asset.GoogleDriveLink,
|
||||
asset.GoogleDriveWorkspaceFolderPath,
|
||||
asset.PreviewUrl,
|
||||
asset.CurrentRevisionNumber,
|
||||
asset.CreatedAt,
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Organizations.Data;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||
|
||||
internal record WorkspaceDamBackingStoreDto(
|
||||
string Type,
|
||||
bool IsConfigured,
|
||||
string? RootFolderId,
|
||||
string? RootFolderName,
|
||||
string? RootFolderUrl);
|
||||
|
||||
internal record WorkspaceDamFolderDto(
|
||||
string Name,
|
||||
string Path);
|
||||
|
||||
internal record WorkspaceDamDto(
|
||||
Guid WorkspaceId,
|
||||
Guid OrganizationId,
|
||||
string WorkspaceName,
|
||||
string WorkspaceSlug,
|
||||
WorkspaceDamBackingStoreDto BackingStore,
|
||||
WorkspaceDamFolderDto? Folder,
|
||||
IReadOnlyCollection<AssetDto> Assets);
|
||||
|
||||
internal class GetWorkspaceDamHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService)
|
||||
: EndpointWithoutRequest<WorkspaceDamDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/workspaces/{workspaceId:guid}/dam");
|
||||
Options(o => o.WithTags("Assets"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
Guid workspaceId = Route<Guid>("workspaceId");
|
||||
|
||||
Workspace? workspace = await dbContext.Workspaces
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == workspaceId, ct);
|
||||
if (workspace is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
IReadOnlyCollection<Guid> accessibleWorkspaceIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||
if (!accessibleWorkspaceIds.Contains(workspace.Id))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
Organization? organization = await dbContext.Organizations
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == workspace.OrganizationId, ct);
|
||||
if (organization is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
WorkspaceDamBackingStoreDto backingStore = new(
|
||||
organization.IsGoogleDriveDamEnabled ? "GoogleDrive" : "Unconfigured",
|
||||
organization.IsGoogleDriveDamEnabled,
|
||||
organization.GoogleDriveRootFolderId,
|
||||
organization.GoogleDriveRootFolderName,
|
||||
organization.GoogleDriveRootFolderUrl);
|
||||
|
||||
WorkspaceDamFolderDto? folder = organization.IsGoogleDriveDamEnabled
|
||||
? new WorkspaceDamFolderDto(
|
||||
workspace.Slug,
|
||||
$"{organization.GoogleDriveRootFolderName}/{workspace.Slug}")
|
||||
: null;
|
||||
|
||||
List<AssetDto> assets = await dbContext.Assets
|
||||
.Where(asset => asset.WorkspaceId == workspace.Id)
|
||||
.OrderBy(asset => asset.DisplayName)
|
||||
.Select(asset => new AssetDto(
|
||||
asset.Id,
|
||||
asset.WorkspaceId,
|
||||
asset.ContentItemId,
|
||||
asset.AssetType,
|
||||
asset.SourceType,
|
||||
asset.DisplayName,
|
||||
asset.GoogleDriveFileId,
|
||||
asset.GoogleDriveLink,
|
||||
asset.GoogleDriveWorkspaceFolderPath,
|
||||
asset.PreviewUrl,
|
||||
asset.CurrentRevisionNumber,
|
||||
asset.CreatedAt,
|
||||
dbContext.AssetRevisions
|
||||
.Where(revision => revision.AssetId == asset.Id)
|
||||
.OrderByDescending(revision => revision.RevisionNumber)
|
||||
.Select(revision => new AssetRevisionDto(
|
||||
revision.Id,
|
||||
revision.AssetId,
|
||||
revision.RevisionNumber,
|
||||
revision.SourceReference,
|
||||
revision.PreviewUrl,
|
||||
revision.Notes,
|
||||
revision.CreatedByUserId,
|
||||
revision.CreatedAt))
|
||||
.ToList()))
|
||||
.ToListAsync(ct);
|
||||
|
||||
await SendOkAsync(
|
||||
new WorkspaceDamDto(
|
||||
workspace.Id,
|
||||
workspace.OrganizationId,
|
||||
workspace.Name,
|
||||
workspace.Slug,
|
||||
backingStore,
|
||||
folder,
|
||||
assets),
|
||||
ct);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,10 @@ internal class Organization
|
||||
public Guid Id { get; init; }
|
||||
public required string Name { get; set; }
|
||||
public string? LogoUrl { get; set; }
|
||||
public bool IsGoogleDriveDamEnabled { get; set; }
|
||||
public string? GoogleDriveRootFolderId { get; set; }
|
||||
public string? GoogleDriveRootFolderName { get; set; }
|
||||
public string? GoogleDriveRootFolderUrl { get; set; }
|
||||
public Guid MembershipTierId { get; set; } = OrganizationMembershipTierSeed.FreeId;
|
||||
public Guid OwnerUserId { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
|
||||
@@ -12,6 +12,10 @@ internal static class OrganizationModelConfiguration
|
||||
organization.HasKey(x => x.Id);
|
||||
organization.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||
organization.Property(x => x.LogoUrl).HasMaxLength(2048);
|
||||
organization.Property(x => x.IsGoogleDriveDamEnabled).HasDefaultValue(false);
|
||||
organization.Property(x => x.GoogleDriveRootFolderId).HasMaxLength(256);
|
||||
organization.Property(x => x.GoogleDriveRootFolderName).HasMaxLength(256);
|
||||
organization.Property(x => x.GoogleDriveRootFolderUrl).HasMaxLength(2048);
|
||||
organization.Property(x => x.MembershipTierId)
|
||||
.HasDefaultValue(OrganizationMembershipTierSeed.FreeId);
|
||||
organization.Property(x => x.CreatedAt)
|
||||
|
||||
@@ -16,6 +16,7 @@ internal record OrganizationDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? LogoUrl,
|
||||
OrganizationGoogleDriveDamConfigurationDto GoogleDriveDam,
|
||||
OrganizationMembershipTierDto? MembershipTier,
|
||||
Guid OwnerUserId,
|
||||
IReadOnlyCollection<string> CurrentUserPermissions,
|
||||
@@ -38,6 +39,7 @@ internal record OrganizationDto(
|
||||
organization.Id,
|
||||
organization.Name,
|
||||
organization.LogoUrl,
|
||||
OrganizationGoogleDriveDamConfigurationDto.FromOrganization(organization),
|
||||
membershipTier,
|
||||
organization.OwnerUserId,
|
||||
currentUserPermissions,
|
||||
@@ -49,6 +51,22 @@ internal record OrganizationDto(
|
||||
}
|
||||
}
|
||||
|
||||
internal record OrganizationGoogleDriveDamConfigurationDto(
|
||||
bool IsEnabled,
|
||||
string? RootFolderId,
|
||||
string? RootFolderName,
|
||||
string? RootFolderUrl)
|
||||
{
|
||||
public static OrganizationGoogleDriveDamConfigurationDto FromOrganization(Organization organization)
|
||||
{
|
||||
return new OrganizationGoogleDriveDamConfigurationDto(
|
||||
organization.IsGoogleDriveDamEnabled,
|
||||
organization.GoogleDriveRootFolderId,
|
||||
organization.GoogleDriveRootFolderName,
|
||||
organization.GoogleDriveRootFolderUrl);
|
||||
}
|
||||
}
|
||||
|
||||
internal record OrganizationMembershipTierDto(
|
||||
Guid Id,
|
||||
string Key,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
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;
|
||||
|
||||
internal record UpdateGoogleDriveDamConfigurationRequest(
|
||||
bool IsEnabled,
|
||||
string? RootFolderId,
|
||||
string? RootFolderName,
|
||||
string? RootFolderUrl);
|
||||
|
||||
internal class UpdateGoogleDriveDamConfigurationRequestValidator
|
||||
: Validator<UpdateGoogleDriveDamConfigurationRequest>
|
||||
{
|
||||
public UpdateGoogleDriveDamConfigurationRequestValidator()
|
||||
{
|
||||
When(x => x.IsEnabled, () =>
|
||||
{
|
||||
RuleFor(x => x.RootFolderId).NotEmpty().MaximumLength(256);
|
||||
RuleFor(x => x.RootFolderName).NotEmpty().MaximumLength(256);
|
||||
RuleFor(x => x.RootFolderUrl).NotEmpty().MaximumLength(2048);
|
||||
});
|
||||
|
||||
RuleFor(x => x.RootFolderId).MaximumLength(256);
|
||||
RuleFor(x => x.RootFolderName).MaximumLength(256);
|
||||
RuleFor(x => x.RootFolderUrl).MaximumLength(2048);
|
||||
}
|
||||
}
|
||||
|
||||
internal class UpdateGoogleDriveDamConfigurationHandler(
|
||||
AppDbContext dbContext,
|
||||
OrganizationAccessService organizationAccessService)
|
||||
: Endpoint<UpdateGoogleDriveDamConfigurationRequest, OrganizationGoogleDriveDamConfigurationDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/api/organizations/{organizationId:guid}/google-drive-dam");
|
||||
Options(o => o.WithTags("Organizations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UpdateGoogleDriveDamConfigurationRequest request, 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.HasOrganizationPermissionAsync(
|
||||
User,
|
||||
organizationId,
|
||||
OrganizationPermissions.ManageConnectors,
|
||||
ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
organization.IsGoogleDriveDamEnabled = request.IsEnabled;
|
||||
organization.GoogleDriveRootFolderId = NormalizeOptional(request.RootFolderId);
|
||||
organization.GoogleDriveRootFolderName = NormalizeOptional(request.RootFolderName);
|
||||
organization.GoogleDriveRootFolderUrl = NormalizeOptional(request.RootFolderUrl);
|
||||
|
||||
if (!organization.IsGoogleDriveDamEnabled)
|
||||
{
|
||||
organization.GoogleDriveRootFolderId = null;
|
||||
organization.GoogleDriveRootFolderName = null;
|
||||
organization.GoogleDriveRootFolderUrl = null;
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(OrganizationGoogleDriveDamConfigurationDto.FromOrganization(organization), ct);
|
||||
}
|
||||
|
||||
private static string? NormalizeOptional(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ internal class Workspace
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
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; }
|
||||
|
||||
@@ -12,6 +12,7 @@ internal static class WorkspaceModelConfiguration
|
||||
workspace.ToTable("Workspaces");
|
||||
workspace.HasKey(x => x.Id);
|
||||
workspace.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||
workspace.Property(x => x.Slug).HasMaxLength(96).IsRequired();
|
||||
workspace.Property(x => x.LogoUrl).HasMaxLength(2048);
|
||||
workspace.Property(x => x.TimeZone).HasMaxLength(128).IsRequired();
|
||||
workspace.Property(x => x.ApprovalMode).HasMaxLength(32).IsRequired().HasDefaultValue("Required");
|
||||
@@ -22,6 +23,7 @@ internal static class WorkspaceModelConfiguration
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
workspace.HasIndex(x => x.OrganizationId);
|
||||
workspace.HasIndex(x => new { x.OrganizationId, x.Slug }).IsUnique();
|
||||
workspace.HasIndex(x => x.OwnerUserId);
|
||||
workspace.HasOne<Organization>()
|
||||
.WithMany()
|
||||
|
||||
@@ -3,12 +3,14 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
using Socialize.Api.Modules.Workspaces.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
||||
|
||||
internal record CreateWorkspaceRequest(
|
||||
Guid OrganizationId,
|
||||
string Name,
|
||||
string? Slug,
|
||||
string TimeZone);
|
||||
|
||||
internal class CreateWorkspaceRequestValidator
|
||||
@@ -18,6 +20,7 @@ internal class CreateWorkspaceRequestValidator
|
||||
{
|
||||
RuleFor(x => x.OrganizationId).NotEmpty();
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||
RuleFor(x => x.Slug).MaximumLength(96);
|
||||
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(128);
|
||||
}
|
||||
}
|
||||
@@ -51,6 +54,12 @@ internal class CreateWorkspaceHandler(
|
||||
}
|
||||
|
||||
string normalizedName = request.Name.Trim();
|
||||
string normalizedSlug = await WorkspaceSlugGenerator.CreateUniqueAsync(
|
||||
dbContext,
|
||||
request.OrganizationId,
|
||||
string.IsNullOrWhiteSpace(request.Slug) ? normalizedName : request.Slug,
|
||||
null,
|
||||
ct);
|
||||
string normalizedTimeZone = request.TimeZone.Trim();
|
||||
|
||||
Workspace workspace = new()
|
||||
@@ -58,6 +67,7 @@ internal class CreateWorkspaceHandler(
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = request.OrganizationId,
|
||||
Name = normalizedName,
|
||||
Slug = normalizedSlug,
|
||||
OwnerUserId = User.GetUserId(),
|
||||
TimeZone = normalizedTimeZone,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
|
||||
@@ -21,6 +21,7 @@ internal record WorkspaceDto(
|
||||
Guid Id,
|
||||
Guid OrganizationId,
|
||||
string Name,
|
||||
string Slug,
|
||||
string? LogoUrl,
|
||||
string TimeZone,
|
||||
string ApprovalMode,
|
||||
@@ -38,6 +39,7 @@ internal record WorkspaceDto(
|
||||
workspace.Id,
|
||||
workspace.OrganizationId,
|
||||
workspace.Name,
|
||||
workspace.Slug,
|
||||
workspace.LogoUrl,
|
||||
workspace.TimeZone,
|
||||
workspace.ApprovalMode,
|
||||
|
||||
@@ -5,6 +5,7 @@ using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Approvals.Services;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
using Socialize.Api.Modules.Workspaces.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.Workspaces.Handlers;
|
||||
|
||||
@@ -17,6 +18,7 @@ internal record UpdateApprovalStepConfigurationRequest(
|
||||
|
||||
internal record UpdateWorkspaceRequest(
|
||||
string Name,
|
||||
string? Slug,
|
||||
string TimeZone,
|
||||
string? ApprovalMode,
|
||||
bool? SchedulePostsAutomaticallyOnApproval,
|
||||
@@ -32,6 +34,7 @@ internal class UpdateWorkspaceRequestValidator
|
||||
public UpdateWorkspaceRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||
RuleFor(x => x.Slug).MaximumLength(96);
|
||||
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(128);
|
||||
RuleFor(x => x.ApprovalMode)
|
||||
.Must(mode => string.IsNullOrWhiteSpace(mode) || AllowedApprovalModes.Contains(mode.Trim()))
|
||||
@@ -106,6 +109,12 @@ internal class UpdateWorkspaceHandler(
|
||||
}
|
||||
|
||||
workspace.Name = request.Name.Trim();
|
||||
workspace.Slug = await WorkspaceSlugGenerator.CreateUniqueAsync(
|
||||
dbContext,
|
||||
workspace.OrganizationId,
|
||||
string.IsNullOrWhiteSpace(request.Slug) ? workspace.Name : request.Slug,
|
||||
workspace.Id,
|
||||
ct);
|
||||
workspace.TimeZone = request.TimeZone.Trim();
|
||||
workspace.ApprovalMode = nextApprovalMode;
|
||||
workspace.SchedulePostsAutomaticallyOnApproval = request.SchedulePostsAutomaticallyOnApproval ?? workspace.SchedulePostsAutomaticallyOnApproval;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Text;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.Workspaces.Services;
|
||||
|
||||
internal static class WorkspaceSlugGenerator
|
||||
{
|
||||
public static string Normalize(string value)
|
||||
{
|
||||
#pragma warning disable CA1308 // Workspace slugs are intentionally lowercase external folder names.
|
||||
string trimmed = value.Trim().ToLowerInvariant();
|
||||
#pragma warning restore CA1308
|
||||
var builder = new StringBuilder(trimmed.Length);
|
||||
bool previousWasSeparator = false;
|
||||
|
||||
foreach (char character in trimmed)
|
||||
{
|
||||
if (char.IsLetterOrDigit(character))
|
||||
{
|
||||
builder.Append(character);
|
||||
previousWasSeparator = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (previousWasSeparator)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append('-');
|
||||
previousWasSeparator = true;
|
||||
}
|
||||
|
||||
string slug = builder.ToString().Trim('-');
|
||||
return string.IsNullOrWhiteSpace(slug) ? "workspace" : slug[..Math.Min(slug.Length, 96)];
|
||||
}
|
||||
|
||||
public static async Task<string> CreateUniqueAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid organizationId,
|
||||
string source,
|
||||
Guid? excludingWorkspaceId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
string baseSlug = Normalize(source);
|
||||
string candidate = baseSlug;
|
||||
int suffix = 2;
|
||||
|
||||
while (await dbContext.Workspaces.AnyAsync(
|
||||
workspace => workspace.OrganizationId == organizationId &&
|
||||
workspace.Slug == candidate &&
|
||||
(!excludingWorkspaceId.HasValue || workspace.Id != excludingWorkspaceId.Value),
|
||||
ct))
|
||||
{
|
||||
string suffixText = $"-{suffix}";
|
||||
int maxBaseLength = 96 - suffixText.Length;
|
||||
candidate = $"{baseSlug[..Math.Min(baseSlug.Length, maxBaseLength)]}{suffixText}";
|
||||
suffix++;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user