feat: pivot to social media workflow app
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-04-24 12:58:35 -04:00
parent 0f4acc1b6d
commit df3e602015
349 changed files with 18685 additions and 16010 deletions

View File

@@ -0,0 +1,102 @@
using Socialize.Infrastructure.Security;
using Socialize.Modules.Notifications.Contracts;
namespace Socialize.Modules.Assets.Handlers;
public record CreateAssetRevisionRequest(
string SourceReference,
string? PreviewUrl,
string? Notes);
public class CreateAssetRevisionRequestValidator
: Validator<CreateAssetRevisionRequest>
{
public CreateAssetRevisionRequestValidator()
{
RuleFor(x => x.SourceReference).NotEmpty().MaximumLength(2048);
RuleFor(x => x.PreviewUrl).MaximumLength(2048);
RuleFor(x => x.Notes).MaximumLength(1024);
}
}
public class CreateAssetRevisionHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService,
INotificationEventWriter notificationEventWriter)
: Endpoint<CreateAssetRevisionRequest, AssetRevisionDto>
{
public override void Configure()
{
Post("/api/assets/{id}/revisions");
Options(o => o.WithTags("Assets"));
}
public override async Task HandleAsync(CreateAssetRevisionRequest request, CancellationToken ct)
{
Guid id = Route<Guid>("id");
Asset? asset = await dbContext.Assets.SingleOrDefaultAsync(candidate => candidate.Id == id, ct);
if (asset is null)
{
await SendNotFoundAsync(ct);
return;
}
ContentItem? contentItem = await dbContext.ContentItems
.SingleOrDefaultAsync(candidate => candidate.Id == asset.ContentItemId, ct);
if (contentItem is not null &&
!accessScopeService.CanContributeToProject(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
{
await SendForbiddenAsync(ct);
return;
}
int revisionNumber = asset.CurrentRevisionNumber + 1;
asset.CurrentRevisionNumber = revisionNumber;
asset.PreviewUrl = string.IsNullOrWhiteSpace(request.PreviewUrl) ? asset.PreviewUrl : request.PreviewUrl.Trim();
AssetRevision revision = new()
{
Id = Guid.NewGuid(),
AssetId = asset.Id,
RevisionNumber = revisionNumber,
SourceReference = request.SourceReference.Trim(),
PreviewUrl = string.IsNullOrWhiteSpace(request.PreviewUrl) ? null : request.PreviewUrl.Trim(),
Notes = string.IsNullOrWhiteSpace(request.Notes) ? null : request.Notes.Trim(),
CreatedByUserId = User.GetUserId(),
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.AssetRevisions.Add(revision);
await dbContext.SaveChangesAsync(ct);
if (contentItem is not null)
{
await notificationEventWriter.WriteAsync(
new NotificationEventWriteModel(
asset.WorkspaceId,
asset.ContentItemId,
"asset.revision.created",
"AssetRevision",
revision.Id,
$"A new asset revision was added to {asset.DisplayName}.",
User.GetUserId(),
User.GetEmail(),
$$"""{"revisionNumber":"{{revisionNumber}}"}"""),
ct);
}
AssetRevisionDto dto = new(
revision.Id,
revision.AssetId,
revision.RevisionNumber,
revision.SourceReference,
revision.PreviewUrl,
revision.Notes,
revision.CreatedByUserId,
revision.CreatedAt);
await SendAsync(dto, StatusCodes.Status201Created, ct);
}
}

View File

@@ -0,0 +1,130 @@
using Socialize.Infrastructure.Security;
using Socialize.Modules.Notifications.Contracts;
namespace Socialize.Modules.Assets.Handlers;
public record CreateGoogleDriveAssetRequest(
Guid WorkspaceId,
Guid ContentItemId,
string AssetType,
string DisplayName,
string GoogleDriveFileId,
string GoogleDriveLink,
string? PreviewUrl);
public class CreateGoogleDriveAssetRequestValidator
: Validator<CreateGoogleDriveAssetRequest>
{
public CreateGoogleDriveAssetRequestValidator()
{
RuleFor(x => x.WorkspaceId).NotEmpty();
RuleFor(x => x.ContentItemId).NotEmpty();
RuleFor(x => x.AssetType).NotEmpty().MaximumLength(64);
RuleFor(x => x.DisplayName).NotEmpty().MaximumLength(256);
RuleFor(x => x.GoogleDriveFileId).NotEmpty().MaximumLength(256);
RuleFor(x => x.GoogleDriveLink).NotEmpty().MaximumLength(2048);
RuleFor(x => x.PreviewUrl).MaximumLength(2048);
}
}
public class CreateGoogleDriveAssetHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService,
INotificationEventWriter notificationEventWriter)
: Endpoint<CreateGoogleDriveAssetRequest, AssetDto>
{
public override void Configure()
{
Post("/api/assets/google-drive");
Options(o => o.WithTags("Assets"));
}
public override async Task HandleAsync(CreateGoogleDriveAssetRequest request, CancellationToken ct)
{
ContentItem? 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.CanContributeToProject(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
{
await SendForbiddenAsync(ct);
return;
}
Asset asset = new()
{
Id = Guid.NewGuid(),
WorkspaceId = request.WorkspaceId,
ContentItemId = request.ContentItemId,
AssetType = request.AssetType.Trim(),
SourceType = "GoogleDrive",
DisplayName = request.DisplayName.Trim(),
GoogleDriveFileId = request.GoogleDriveFileId.Trim(),
GoogleDriveLink = request.GoogleDriveLink.Trim(),
PreviewUrl = string.IsNullOrWhiteSpace(request.PreviewUrl) ? null : request.PreviewUrl.Trim(),
CurrentRevisionNumber = 1,
CreatedAt = DateTimeOffset.UtcNow,
};
AssetRevision revision = new()
{
Id = Guid.NewGuid(),
AssetId = asset.Id,
RevisionNumber = 1,
SourceReference = asset.GoogleDriveLink,
PreviewUrl = asset.PreviewUrl,
CreatedAt = DateTimeOffset.UtcNow,
};
dbContext.Assets.Add(asset);
dbContext.AssetRevisions.Add(revision);
await dbContext.SaveChangesAsync(ct);
await notificationEventWriter.WriteAsync(
new NotificationEventWriteModel(
asset.WorkspaceId,
asset.ContentItemId,
"asset.google-drive-linked",
"Asset",
asset.Id,
$"Google Drive asset {asset.DisplayName} was linked to {contentItem.Title}.",
null,
null,
$$"""{"googleDriveFileId":"{{asset.GoogleDriveFileId}}"}"""),
ct);
AssetDto dto = new(
asset.Id,
asset.WorkspaceId,
asset.ContentItemId,
asset.AssetType,
asset.SourceType,
asset.DisplayName,
asset.GoogleDriveFileId,
asset.GoogleDriveLink,
asset.PreviewUrl,
asset.CurrentRevisionNumber,
asset.CreatedAt,
[
new AssetRevisionDto(
revision.Id,
revision.AssetId,
revision.RevisionNumber,
revision.SourceReference,
revision.PreviewUrl,
revision.Notes,
revision.CreatedByUserId,
revision.CreatedAt)
]);
await SendAsync(dto, StatusCodes.Status201Created, ct);
}
}

View File

@@ -0,0 +1,89 @@
using Socialize.Infrastructure.Security;
namespace Socialize.Modules.Assets.Handlers;
public record GetAssetsRequest(Guid ContentItemId);
public record AssetRevisionDto(
Guid Id,
Guid AssetId,
int RevisionNumber,
string SourceReference,
string? PreviewUrl,
string? Notes,
Guid? CreatedByUserId,
DateTimeOffset CreatedAt);
public record AssetDto(
Guid Id,
Guid WorkspaceId,
Guid ContentItemId,
string AssetType,
string SourceType,
string DisplayName,
string? GoogleDriveFileId,
string? GoogleDriveLink,
string? PreviewUrl,
int CurrentRevisionNumber,
DateTimeOffset CreatedAt,
IReadOnlyCollection<AssetRevisionDto> Revisions);
public class GetAssetsHandler(
AppDbContext dbContext,
AccessScopeService accessScopeService)
: Endpoint<GetAssetsRequest, IReadOnlyCollection<AssetDto>>
{
public override void Configure()
{
Get("/api/assets");
Options(o => o.WithTags("Assets"));
}
public override async Task HandleAsync(GetAssetsRequest request, CancellationToken ct)
{
ContentItem? item = await dbContext.ContentItems
.SingleOrDefaultAsync(candidate => candidate.Id == request.ContentItemId, ct);
if (item is null)
{
await SendNotFoundAsync(ct);
return;
}
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
{
await SendForbiddenAsync(ct);
return;
}
List<AssetDto> assets = await dbContext.Assets
.Where(asset => asset.ContentItemId == request.ContentItemId)
.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.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(assets, ct);
}
}