Compare commits
24 Commits
20f8a14bfb
...
approval-w
| Author | SHA1 | Date | |
|---|---|---|---|
| df0409d7f6 | |||
| 5077f557f4 | |||
| 1722d65d22 | |||
| 14023e65d5 | |||
| 237b1a4242 | |||
| ace0279bd0 | |||
| 07458c1541 | |||
| a9bfdc460d | |||
| 258554f9d4 | |||
| 6731fb5d3a | |||
| 5aaddbca40 | |||
| 1263e28c00 | |||
| 4873f39192 | |||
| cb6948aa14 | |||
| f9960b4fc9 | |||
| 2e4c16621d | |||
| 60ce08ee86 | |||
| 0f3652c1a1 | |||
| 63738ad027 | |||
| 6177eec2bf | |||
| b51b8b4185 | |||
| d222e33667 | |||
| fcd80cd30f | |||
| 43bcf449fd |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,6 +33,7 @@ dist/
|
|||||||
*.local
|
*.local
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
|
App_Data/
|
||||||
|
|
||||||
# Local SSL certificates
|
# Local SSL certificates
|
||||||
*.pem
|
*.pem
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ Update OpenAPI:
|
|||||||
- `Comments`: discussion threads on reviewable work.
|
- `Comments`: discussion threads on reviewable work.
|
||||||
- `Approvals`: review decisions and workflow state transitions.
|
- `Approvals`: review decisions and workflow state transitions.
|
||||||
- `Notifications`: activity feed and unread workflow notifications.
|
- `Notifications`: activity feed and unread workflow notifications.
|
||||||
|
- `Feedback`: product feedback reports, screenshots, comments, activity, and developer review workflows.
|
||||||
|
|
||||||
## Task Discipline
|
## Task Discipline
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
# PROMPT TEMPLATES
|
# PROMPT TEMPLATES
|
||||||
|
I need you to help me write a feature. First, we need to define it, so you will ask me questions one-by-one to make sure we have a shared understanding of the scope
|
||||||
|
and expectating. - The feature we want is a way for your clients to report bugs/suggestions/requests from within our app. It should not be intrusive. It should allow
|
||||||
|
them to take a screen capture, put annotation, describe their request and/or issue. Then, as a dev, i will want to collect and review them.
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
This document standardizes how we interact with AI coding agents (Codex, Claude, etc).
|
This document standardizes how we interact with AI coding agents (Codex, Claude, etc).
|
||||||
@@ -324,4 +327,4 @@ scripts/ai-task review docs/tasks/TASK-XXX.md
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
End of document.
|
End of document.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Common.Domain;
|
namespace Socialize.Api.Common.Domain;
|
||||||
|
|
||||||
public abstract class Entity
|
public abstract class Entity
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Socialize.Modules.Approvals.Data;
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
using Socialize.Modules.Assets.Data;
|
using Socialize.Api.Modules.Assets.Data;
|
||||||
using Socialize.Modules.Clients.Data;
|
using Socialize.Api.Modules.Clients.Data;
|
||||||
using Socialize.Modules.Comments.Data;
|
using Socialize.Api.Modules.Comments.Data;
|
||||||
using Socialize.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Modules.Identity.Data;
|
using Socialize.Api.Modules.Feedback.Data;
|
||||||
using Socialize.Modules.Notifications.Data;
|
using Socialize.Api.Modules.Identity.Data;
|
||||||
using Socialize.Modules.Projects.Data;
|
using Socialize.Api.Modules.Notifications.Data;
|
||||||
using Socialize.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Projects.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Data;
|
namespace Socialize.Api.Data;
|
||||||
|
|
||||||
public class AppDbContext(
|
public class AppDbContext(
|
||||||
DbContextOptions<AppDbContext> options)
|
DbContextOptions<AppDbContext> options)
|
||||||
@@ -24,21 +26,29 @@ public class AppDbContext(
|
|||||||
public DbSet<Asset> Assets => Set<Asset>();
|
public DbSet<Asset> Assets => Set<Asset>();
|
||||||
public DbSet<AssetRevision> AssetRevisions => Set<AssetRevision>();
|
public DbSet<AssetRevision> AssetRevisions => Set<AssetRevision>();
|
||||||
public DbSet<Comment> Comments => Set<Comment>();
|
public DbSet<Comment> Comments => Set<Comment>();
|
||||||
|
public DbSet<ApprovalWorkflowInstance> ApprovalWorkflowInstances => Set<ApprovalWorkflowInstance>();
|
||||||
public DbSet<ApprovalRequest> ApprovalRequests => Set<ApprovalRequest>();
|
public DbSet<ApprovalRequest> ApprovalRequests => Set<ApprovalRequest>();
|
||||||
public DbSet<ApprovalDecision> ApprovalDecisions => Set<ApprovalDecision>();
|
public DbSet<ApprovalDecision> ApprovalDecisions => Set<ApprovalDecision>();
|
||||||
|
public DbSet<WorkspaceApprovalStepConfiguration> WorkspaceApprovalStepConfigurations => Set<WorkspaceApprovalStepConfiguration>();
|
||||||
public DbSet<NotificationEvent> NotificationEvents => Set<NotificationEvent>();
|
public DbSet<NotificationEvent> NotificationEvents => Set<NotificationEvent>();
|
||||||
|
public DbSet<FeedbackReport> FeedbackReports => Set<FeedbackReport>();
|
||||||
|
public DbSet<FeedbackTag> FeedbackTags => Set<FeedbackTag>();
|
||||||
|
public DbSet<FeedbackScreenshot> FeedbackScreenshots => Set<FeedbackScreenshot>();
|
||||||
|
public DbSet<FeedbackComment> FeedbackComments => Set<FeedbackComment>();
|
||||||
|
public DbSet<FeedbackActivityEntry> FeedbackActivityEntries => Set<FeedbackActivityEntry>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
modelBuilder.ConfigureWorkspacesModule();
|
builder.ConfigureWorkspacesModule();
|
||||||
modelBuilder.ConfigureClientsModule();
|
builder.ConfigureClientsModule();
|
||||||
modelBuilder.ConfigureProjectsModule();
|
builder.ConfigureProjectsModule();
|
||||||
modelBuilder.ConfigureContentItemsModule();
|
builder.ConfigureContentItemsModule();
|
||||||
modelBuilder.ConfigureAssetsModule();
|
builder.ConfigureAssetsModule();
|
||||||
modelBuilder.ConfigureCommentsModule();
|
builder.ConfigureCommentsModule();
|
||||||
modelBuilder.ConfigureApprovalsModule();
|
builder.ConfigureApprovalsModule();
|
||||||
modelBuilder.ConfigureNotificationsModule();
|
builder.ConfigureNotificationsModule();
|
||||||
|
builder.ConfigureFeedbackModule();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Socialize.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Facebook;
|
using Microsoft.AspNetCore.Authentication.Facebook;
|
||||||
using Microsoft.AspNetCore.Authentication.Google;
|
using Microsoft.AspNetCore.Authentication.Google;
|
||||||
@@ -49,7 +50,7 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
using IServiceScope scope = app.ApplicationServices.CreateScope();
|
using IServiceScope scope = app.ApplicationServices.CreateScope();
|
||||||
await using AppDbContext context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
await using AppDbContext context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
await context.Database.EnsureCreatedAsync(cancellationToken);
|
await context.Database.MigrateAsync(cancellationToken);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,2 @@
|
|||||||
global using FluentValidation;
|
global using FluentValidation;
|
||||||
global using FastEndpoints;
|
|
||||||
global using JetBrains.Annotations;
|
global using JetBrains.Annotations;
|
||||||
global using Microsoft.EntityFrameworkCore;
|
|
||||||
global using Socialize.Data;
|
|
||||||
global using Socialize.Modules.Approvals.Data;
|
|
||||||
global using Socialize.Modules.Assets.Data;
|
|
||||||
global using Socialize.Modules.Clients.Data;
|
|
||||||
global using Socialize.Modules.Comments.Data;
|
|
||||||
global using Socialize.Modules.ContentItems.Data;
|
|
||||||
global using Socialize.Modules.Identity.Data;
|
|
||||||
global using Socialize.Modules.Notifications.Data;
|
|
||||||
global using Socialize.Modules.Projects.Data;
|
|
||||||
global using Socialize.Modules.Workspaces.Data;
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
||||||
|
|
||||||
|
public sealed class LocalBlobStorageOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "LocalBlobStorage";
|
||||||
|
|
||||||
|
public string RootPath { get; set; } = "App_Data/blob-storage";
|
||||||
|
|
||||||
|
public string RequestPath { get; set; } = "/api/storage";
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class CommonFileNames
|
public static class CommonFileNames
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
internal static class ContainerNames
|
internal static class ContainerNames
|
||||||
{
|
{
|
||||||
public const string Users = "users";
|
public const string Users = "users";
|
||||||
public const string Clients = "clients";
|
public const string Clients = "clients";
|
||||||
|
public const string Workspaces = "workspaces";
|
||||||
public const string Creators = "creators";
|
public const string Creators = "creators";
|
||||||
|
public const string Feedback = "feedback";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class ContentTypes
|
public static class ContentTypes
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public interface IBlobStorage
|
public interface IBlobStorage
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
namespace Socialize.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class SubDirectoryNames
|
public static class SubDirectoryNames
|
||||||
{
|
{
|
||||||
public const string Profile = "profile";
|
public const string Profile = "profile";
|
||||||
public const string Contents = "contents";
|
public const string Contents = "contents";
|
||||||
public const string Albums = "albums";
|
public const string Albums = "albums";
|
||||||
|
public const string FeedbackScreenshots = "screenshots";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
using Azure;
|
|
||||||
using Azure.Storage.Blobs;
|
|
||||||
using Azure.Storage.Blobs.Models;
|
|
||||||
using Socialize.Infrastructure.BlobStorage.Contracts;
|
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.BlobStorage.Services;
|
|
||||||
|
|
||||||
public class AzureBlobStorage : IBlobStorage
|
|
||||||
{
|
|
||||||
private const long MaxUploadSize = 10 * 1024 * 1024; // 10 MB in bytes
|
|
||||||
|
|
||||||
private readonly BlobServiceClient _blobServiceClient;
|
|
||||||
private readonly ILogger<AzureBlobStorage> _logger;
|
|
||||||
|
|
||||||
public AzureBlobStorage(IConfiguration configuration, ILogger<AzureBlobStorage> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
string? connectionString = configuration.GetConnectionString("AzureBlob");
|
|
||||||
_blobServiceClient = new BlobServiceClient(connectionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Upload a file to microsoft azure blob storage.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="containerName">The name of the container where the file is stored.</param>
|
|
||||||
/// <param name="blobName">The blob name (path within the container, include the file name).</param>
|
|
||||||
/// <param name="stream"></param>
|
|
||||||
/// <param name="contentType">The content type.</param>
|
|
||||||
/// <param name="ct">The cancellation token</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<string> UploadFileAsync(
|
|
||||||
string containerName,
|
|
||||||
string blobName,
|
|
||||||
Stream stream,
|
|
||||||
string contentType,
|
|
||||||
CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
// Read the file stream into a memory stream to determine the length
|
|
||||||
// WATCH FOR MEMORY USAGE USING THE MEMORY STREAM.
|
|
||||||
stream.Position = 0;
|
|
||||||
|
|
||||||
// Check if the file size exceeds the maximum upload size
|
|
||||||
if (stream.Length > MaxUploadSize)
|
|
||||||
{
|
|
||||||
_logger.LogError(
|
|
||||||
$"Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.");
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate content type
|
|
||||||
if (!ContentTypes.IsAllowed(contentType, stream))
|
|
||||||
{
|
|
||||||
_logger.LogError(
|
|
||||||
$"Blob storage: Unsupported file type {contentType}.");
|
|
||||||
throw new InvalidOperationException("Unsupported file type.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Get a reference to a container
|
|
||||||
BlobContainerClient? containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
|
||||||
|
|
||||||
// Create the container if it does not exist
|
|
||||||
await containerClient.CreateIfNotExistsAsync(
|
|
||||||
PublicAccessType.Blob,
|
|
||||||
cancellationToken: ct);
|
|
||||||
|
|
||||||
// Get a reference to a blob
|
|
||||||
BlobClient? blobClient = containerClient.GetBlobClient(blobName);
|
|
||||||
|
|
||||||
// Define the BlobHttpHeaders to include the content type
|
|
||||||
BlobHttpHeaders blobHttpHeaders = new() { ContentType = contentType };
|
|
||||||
|
|
||||||
// Upload the file
|
|
||||||
Response<BlobContentInfo>? response = await blobClient.UploadAsync(
|
|
||||||
stream,
|
|
||||||
new BlobUploadOptions { HttpHeaders = blobHttpHeaders },
|
|
||||||
ct);
|
|
||||||
|
|
||||||
string fileUri = blobClient.Uri.ToString();
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"""
|
|
||||||
Blob storage: Status [ {ResponseStatus} ]
|
|
||||||
Uploaded [ {BlobName} ] to the container [ {ContainerName} ]
|
|
||||||
with contentType [ {ContentType} ]
|
|
||||||
with a length of [ {StreamLength} bytes ]
|
|
||||||
with the uri [ {FileUri} ]
|
|
||||||
""",
|
|
||||||
response.GetRawResponse().Status.ToString(),
|
|
||||||
blobName,
|
|
||||||
containerName,
|
|
||||||
contentType,
|
|
||||||
stream.Length,
|
|
||||||
fileUri
|
|
||||||
);
|
|
||||||
|
|
||||||
// Return the URI of the uploaded blob
|
|
||||||
return fileUri;
|
|
||||||
}
|
|
||||||
catch (RequestFailedException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError($"Blob storage: Azure Storage request failed: {ex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError($"Blob storage: An error occurred: {ex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Download a file to microsoft's azure blob storage.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="blobName">The blob name (path within the container).</param>
|
|
||||||
/// <param name="containerName">The name of the container where the file is stored. (users)</param>
|
|
||||||
/// <param name="ct">The cancellation token for the request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public async Task<MemoryStream> DownloadFileAsync(
|
|
||||||
string containerName,
|
|
||||||
string blobName,
|
|
||||||
CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Get a reference to a container
|
|
||||||
BlobContainerClient? containerClient = _blobServiceClient.GetBlobContainerClient(containerName);
|
|
||||||
|
|
||||||
// Get a reference to a blob
|
|
||||||
BlobClient? blobClient = containerClient.GetBlobClient(blobName);
|
|
||||||
|
|
||||||
// Download the blob to a stream
|
|
||||||
BlobDownloadInfo download = await blobClient.DownloadAsync(ct);
|
|
||||||
|
|
||||||
MemoryStream memoryStream = new();
|
|
||||||
await download.Content.CopyToAsync(memoryStream, ct);
|
|
||||||
memoryStream.Position = 0; // Ensure the stream is at the beginning
|
|
||||||
|
|
||||||
return memoryStream;
|
|
||||||
}
|
|
||||||
catch (RequestFailedException ex)
|
|
||||||
{
|
|
||||||
_logger.LogError($"Azure Storage request failed: {ex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError($"An error occurred: {ex.Message}");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
||||||
|
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Infrastructure.BlobStorage.Services;
|
||||||
|
|
||||||
|
public sealed class LocalBlobStorage(
|
||||||
|
IWebHostEnvironment environment,
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
IOptions<LocalBlobStorageOptions> options,
|
||||||
|
ILogger<LocalBlobStorage> logger)
|
||||||
|
: IBlobStorage
|
||||||
|
{
|
||||||
|
private const long MaxUploadSize = 10 * 1024 * 1024;
|
||||||
|
private const string ContentTypeMetadataSuffix = ".content-type";
|
||||||
|
|
||||||
|
private readonly LocalBlobStorageOptions _options = options.Value;
|
||||||
|
|
||||||
|
public async Task<string> UploadFileAsync(
|
||||||
|
string containerName,
|
||||||
|
string blobName,
|
||||||
|
Stream stream,
|
||||||
|
string contentType,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
stream.Position = 0;
|
||||||
|
|
||||||
|
if (stream.Length > MaxUploadSize)
|
||||||
|
{
|
||||||
|
logger.LogError("Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.", MaxUploadSize);
|
||||||
|
throw new InvalidOperationException($"Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ContentTypes.IsAllowed(contentType, stream))
|
||||||
|
{
|
||||||
|
logger.LogError("Blob storage: Unsupported file type {ContentType}.", contentType);
|
||||||
|
throw new InvalidOperationException("Unsupported file type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string relativePath = GetSafeRelativePath(containerName, blobName);
|
||||||
|
string filePath = Path.Combine(GetRootPath(), relativePath);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? GetRootPath());
|
||||||
|
|
||||||
|
await using FileStream fileStream = File.Create(filePath);
|
||||||
|
await stream.CopyToAsync(fileStream, ct);
|
||||||
|
await File.WriteAllTextAsync(GetContentTypeMetadataPath(filePath), contentType, ct);
|
||||||
|
|
||||||
|
string fileUri = BuildPublicUrl(relativePath);
|
||||||
|
logger.LogInformation(
|
||||||
|
"Blob storage: Uploaded [{BlobName}] to local container [{ContainerName}] with contentType [{ContentType}] and uri [{FileUri}]",
|
||||||
|
blobName,
|
||||||
|
containerName,
|
||||||
|
contentType,
|
||||||
|
fileUri);
|
||||||
|
|
||||||
|
return fileUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MemoryStream> DownloadFileAsync(
|
||||||
|
string containerName,
|
||||||
|
string blobName,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
string filePath = Path.Combine(GetRootPath(), GetSafeRelativePath(containerName, blobName));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Blob storage: Local file was not found.", blobName);
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryStream memoryStream = new();
|
||||||
|
await using FileStream fileStream = File.OpenRead(filePath);
|
||||||
|
await fileStream.CopyToAsync(memoryStream, ct);
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
|
||||||
|
return memoryStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal string GetRootPath()
|
||||||
|
{
|
||||||
|
if (Path.IsPathRooted(_options.RootPath))
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(_options.RootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.GetFullPath(Path.Combine(environment.ContentRootPath, _options.RootPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string? ReadContentType(string filePath)
|
||||||
|
{
|
||||||
|
string metadataPath = GetContentTypeMetadataPath(filePath);
|
||||||
|
return File.Exists(metadataPath)
|
||||||
|
? File.ReadAllText(metadataPath)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetContentTypeMetadataPath(string filePath)
|
||||||
|
{
|
||||||
|
return $"{filePath}{ContentTypeMetadataSuffix}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetSafeRelativePath(string containerName, string blobName)
|
||||||
|
{
|
||||||
|
if (Path.IsPathRooted(containerName) || Path.IsPathRooted(blobName))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Blob storage: Blob paths must be relative.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string[] pathParts = [containerName, .. blobName.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar])];
|
||||||
|
if (pathParts.Any(part => part is "" or "." or ".."))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Blob storage: Blob paths must not contain relative path segments.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(pathParts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildPublicUrl(string relativePath)
|
||||||
|
{
|
||||||
|
HttpRequest? request = httpContextAccessor.HttpContext?.Request;
|
||||||
|
string requestPath = NormalizeRequestPath(_options.RequestPath);
|
||||||
|
string urlPath = $"{requestPath}/{relativePath.Replace(Path.DirectorySeparatorChar, '/')}";
|
||||||
|
|
||||||
|
if (request is null)
|
||||||
|
{
|
||||||
|
return urlPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{request.Scheme}://{request.Host}{request.PathBase}{urlPath}";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string NormalizeRequestPath(string requestPath)
|
||||||
|
{
|
||||||
|
string normalized = string.IsNullOrWhiteSpace(requestPath)
|
||||||
|
? "/api/storage"
|
||||||
|
: requestPath.Trim();
|
||||||
|
|
||||||
|
return normalized.StartsWith("/", StringComparison.Ordinal)
|
||||||
|
? normalized.TrimEnd('/')
|
||||||
|
: $"/{normalized.TrimEnd('/')}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Infrastructure.Configuration;
|
namespace Socialize.Api.Infrastructure.Configuration;
|
||||||
|
|
||||||
public class WebsiteOptions
|
public class WebsiteOptions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
using Socialize.Infrastructure.BlobStorage.Contracts;
|
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
using Socialize.Infrastructure.BlobStorage.Services;
|
using Socialize.Api.Infrastructure.BlobStorage.Services;
|
||||||
using Socialize.Infrastructure.Configuration;
|
using Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
||||||
using Socialize.Infrastructure.Emailer.Configuration;
|
using Socialize.Api.Infrastructure.Configuration;
|
||||||
using Socialize.Infrastructure.Emailer.Contracts;
|
using Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||||
using Socialize.Infrastructure.Emailer.Services;
|
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||||
using Socialize.Infrastructure.Payments.Stripe.Configuration;
|
using Socialize.Api.Infrastructure.Emailer.Services;
|
||||||
|
using Socialize.Api.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure;
|
namespace Socialize.Api.Infrastructure;
|
||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
@@ -16,7 +17,10 @@ public static class DependencyInjection
|
|||||||
builder.Services.Configure<WebsiteOptions>(
|
builder.Services.Configure<WebsiteOptions>(
|
||||||
builder.Configuration.GetRequiredSection(WebsiteOptions.SectionName));
|
builder.Configuration.GetRequiredSection(WebsiteOptions.SectionName));
|
||||||
|
|
||||||
builder.Services.AddTransient<IBlobStorage, AzureBlobStorage>();
|
builder.Services.Configure<LocalBlobStorageOptions>(
|
||||||
|
builder.Configuration.GetSection(LocalBlobStorageOptions.SectionName));
|
||||||
|
builder.Services.AddTransient<LocalBlobStorage>();
|
||||||
|
builder.Services.AddTransient<IBlobStorage>(services => services.GetRequiredService<LocalBlobStorage>());
|
||||||
builder.Services.Configure<StripeOptions>(
|
builder.Services.Configure<StripeOptions>(
|
||||||
builder.Configuration.GetSection(StripeOptions.ConfigurationSection));
|
builder.Configuration.GetSection(StripeOptions.ConfigurationSection));
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Socialize.Infrastructure.Security;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Modules.Identity.Contracts;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
using Socialize.Modules.Identity.Data;
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
using Socialize.Api.Modules.Identity.Data;
|
||||||
|
using Socialize.Api.Modules.Assets.Data;
|
||||||
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
|
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.Workspaces.Data;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.Development;
|
namespace Socialize.Api.Infrastructure.Development;
|
||||||
|
|
||||||
public static class DevelopmentSeedExtensions
|
public static class DevelopmentSeedExtensions
|
||||||
{
|
{
|
||||||
@@ -41,8 +51,6 @@ public static class DevelopmentSeedExtensions
|
|||||||
UserManager userManager = scope.ServiceProvider.GetRequiredService<UserManager>();
|
UserManager userManager = scope.ServiceProvider.GetRequiredService<UserManager>();
|
||||||
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
|
||||||
await RemoveLegacyDevUserAsync(userManager);
|
|
||||||
|
|
||||||
User manager = await EnsureUserAsync(
|
User manager = await EnsureUserAsync(
|
||||||
userManager,
|
userManager,
|
||||||
id: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
id: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||||
@@ -94,6 +102,21 @@ public static class DevelopmentSeedExtensions
|
|||||||
new Claim(KnownClaims.ProjectScope, ScopedProjectId.ToString()),
|
new Claim(KnownClaims.ProjectScope, ScopedProjectId.ToString()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
User dev = await EnsureUserAsync(
|
||||||
|
userManager,
|
||||||
|
id: Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"),
|
||||||
|
username: "dev",
|
||||||
|
email: "dev@socialize.local",
|
||||||
|
password: "dev",
|
||||||
|
alias: "Socialize Dev",
|
||||||
|
firstname: "Jo",
|
||||||
|
lastname: "Bumble",
|
||||||
|
portraitUrl: "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80",
|
||||||
|
roles: [KnownRoles.Developer, KnownRoles.Administrator, KnownRoles.Manager, KnownRoles.WorkspaceMember],
|
||||||
|
claims:
|
||||||
|
[
|
||||||
|
]);
|
||||||
|
|
||||||
await EnsureWorkspaceDataAsync(
|
await EnsureWorkspaceDataAsync(
|
||||||
manager.Id,
|
manager.Id,
|
||||||
clientUser.Id,
|
clientUser.Id,
|
||||||
@@ -104,19 +127,6 @@ public static class DevelopmentSeedExtensions
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task RemoveLegacyDevUserAsync(UserManager userManager)
|
|
||||||
{
|
|
||||||
User? legacyUser = await userManager.FindByNameAsync("dev")
|
|
||||||
?? await userManager.FindByEmailAsync("dev@socialize.local");
|
|
||||||
|
|
||||||
if (legacyUser is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await userManager.DeleteAsync(legacyUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<User> EnsureUserAsync(
|
private static async Task<User> EnsureUserAsync(
|
||||||
UserManager userManager,
|
UserManager userManager,
|
||||||
Guid id,
|
Guid id,
|
||||||
@@ -297,7 +307,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
"Spring launch hero video",
|
"Spring launch hero video",
|
||||||
"Fresh seasonal menu launch across Instagram and TikTok.",
|
"Fresh seasonal menu launch across Instagram and TikTok.",
|
||||||
"Instagram Reel, TikTok",
|
"Instagram Reel, TikTok",
|
||||||
"In client review",
|
"In approval",
|
||||||
DateTimeOffset.UtcNow.AddDays(3),
|
DateTimeOffset.UtcNow.AddDays(3),
|
||||||
"v3",
|
"v3",
|
||||||
3,
|
3,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Infrastructure.Development;
|
namespace Socialize.Api.Infrastructure.Development;
|
||||||
|
|
||||||
public record DevelopmentSeedOptions
|
public record DevelopmentSeedOptions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Infrastructure.Emailer.Configuration;
|
namespace Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||||
|
|
||||||
public class EmailerOptions
|
public class EmailerOptions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Infrastructure.Emailer.Contracts;
|
namespace Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||||
|
|
||||||
public interface IEmailSender
|
public interface IEmailSender
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Socialize.Infrastructure.Emailer.Contracts;
|
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.Emailer.Services;
|
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
public class LoggerEmailSender(ILogger<IEmailSender> logger)
|
public class LoggerEmailSender(ILogger<IEmailSender> logger)
|
||||||
: IEmailSender
|
: IEmailSender
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
using Socialize.Infrastructure.Emailer.Configuration;
|
using Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||||
using Socialize.Infrastructure.Emailer.Contracts;
|
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using PostmarkDotNet;
|
using PostmarkDotNet;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.Emailer.Services;
|
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
public class PostmarkEmailSender : IEmailSender
|
public class PostmarkEmailSender : IEmailSender
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Socialize.Infrastructure.Emailer.Configuration;
|
using Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||||
using Socialize.Infrastructure.Emailer.Contracts;
|
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.Emailer.Services;
|
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
public class ResendEmailSender : IEmailSender
|
public class ResendEmailSender : IEmailSender
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.Payments.Stripe.Configuration;
|
namespace Socialize.Api.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
|
||||||
public class StripeOptions
|
public class StripeOptions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Socialize.Modules.Identity.Contracts;
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public sealed class AccessScopeService
|
public sealed class AccessScopeService
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class ClaimsPrincipalExtensions
|
public static class ClaimsPrincipalExtensions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Security.Claims;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class JwtTokenHelper
|
public static class JwtTokenHelper
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class KnownClaims
|
public static class KnownClaims
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public class MissingClaimException(
|
public class MissingClaimException(
|
||||||
string claimName)
|
string claimName)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
// If we need to add special characters we can alternate between 2 pools.
|
// If we need to add special characters we can alternate between 2 pools.
|
||||||
public static class PasswordGenerator
|
public static class PasswordGenerator
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class RefreshTokenGenerator
|
public static class RefreshTokenGenerator
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace Socialize.Infrastructure.YouTube;
|
namespace Socialize.Api.Infrastructure.YouTube;
|
||||||
|
|
||||||
public static class YouTubeUrlHelper
|
public static class YouTubeUrlHelper
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
|
|||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
using Socialize.Data;
|
using Socialize.Api.Data;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260430054500_AddWorkspaceLogo")]
|
||||||
|
public partial class AddWorkspaceLogo : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "LogoUrl",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "character varying(2048)",
|
||||||
|
maxLength: 2048,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LogoUrl",
|
||||||
|
table: "Workspaces");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1105
backend/src/Socialize.Api/Migrations/20260430072517_AddFeedbackFoundation.Designer.cs
generated
Normal file
1105
backend/src/Socialize.Api/Migrations/20260430072517_AddFeedbackFoundation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFeedbackFoundation : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FeedbackReports",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
Status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(8000)", maxLength: 8000, nullable: false),
|
||||||
|
ReporterUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ReporterDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
|
ReporterEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
|
SubmittedPath = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||||
|
BrowserUserAgent = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||||
|
ViewportWidth = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
ViewportHeight = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
AppVersion = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||||
|
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
WorkspaceName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
ClientId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
ClientName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
ProjectId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
ProjectName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
ContentItemId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
ContentItemTitle = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
LastActivityAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
CancelledAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
CancelledByUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
CancellationReason = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FeedbackReports", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FeedbackTags",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
NormalizedName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FeedbackTags", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FeedbackTags_FeedbackReports_FeedbackReportId",
|
||||||
|
column: x => x.FeedbackReportId,
|
||||||
|
principalTable: "FeedbackReports",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_LastActivityAt",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "LastActivityAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_ReporterUserId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "ReporterUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_Status",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "Status");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_Type",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "Type");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_WorkspaceId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "WorkspaceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackTags_FeedbackReportId_NormalizedName",
|
||||||
|
table: "FeedbackTags",
|
||||||
|
columns: new[] { "FeedbackReportId", "NormalizedName" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackTags_NormalizedName",
|
||||||
|
table: "FeedbackTags",
|
||||||
|
column: "NormalizedName");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FeedbackTags");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FeedbackReports");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1163
backend/src/Socialize.Api/Migrations/20260430171123_AddFeedbackScreenshots.Designer.cs
generated
Normal file
1163
backend/src/Socialize.Api/Migrations/20260430171123_AddFeedbackScreenshots.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFeedbackScreenshots : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FeedbackScreenshots",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
|
ContentType = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
SizeBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
BlobContainerName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
BlobName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FeedbackScreenshots", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FeedbackScreenshots_FeedbackReports_FeedbackReportId",
|
||||||
|
column: x => x.FeedbackReportId,
|
||||||
|
principalTable: "FeedbackReports",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackScreenshots_FeedbackReportId",
|
||||||
|
table: "FeedbackScreenshots",
|
||||||
|
column: "FeedbackReportId",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FeedbackScreenshots");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1292
backend/src/Socialize.Api/Migrations/20260430171959_AddFeedbackCommentsActivity.Designer.cs
generated
Normal file
1292
backend/src/Socialize.Api/Migrations/20260430171959_AddFeedbackCommentsActivity.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddFeedbackCommentsActivity : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FeedbackActivityEntries",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ActorUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ActorDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
|
ActorEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
|
ActivityType = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
FromValue = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||||
|
ToValue = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||||
|
Note = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FeedbackActivityEntries", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FeedbackActivityEntries_FeedbackReports_FeedbackReportId",
|
||||||
|
column: x => x.FeedbackReportId,
|
||||||
|
principalTable: "FeedbackReports",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "FeedbackComments",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
AuthorUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
AuthorDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
|
AuthorEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
|
AuthorRole = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
Body = table.Column<string>(type: "character varying(8000)", maxLength: 8000, nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_FeedbackComments", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_FeedbackComments_FeedbackReports_FeedbackReportId",
|
||||||
|
column: x => x.FeedbackReportId,
|
||||||
|
principalTable: "FeedbackReports",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackActivityEntries_ActorUserId",
|
||||||
|
table: "FeedbackActivityEntries",
|
||||||
|
column: "ActorUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackActivityEntries_CreatedAt",
|
||||||
|
table: "FeedbackActivityEntries",
|
||||||
|
column: "CreatedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackActivityEntries_FeedbackReportId",
|
||||||
|
table: "FeedbackActivityEntries",
|
||||||
|
column: "FeedbackReportId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackComments_AuthorUserId",
|
||||||
|
table: "FeedbackComments",
|
||||||
|
column: "AuthorUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackComments_CreatedAt",
|
||||||
|
table: "FeedbackComments",
|
||||||
|
column: "CreatedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackComments_FeedbackReportId",
|
||||||
|
table: "FeedbackComments",
|
||||||
|
column: "FeedbackReportId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FeedbackActivityEntries");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "FeedbackComments");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1314
backend/src/Socialize.Api/Migrations/20260501170646_AddWorkspaceApprovalConfiguration.Designer.cs
generated
Normal file
1314
backend/src/Socialize.Api/Migrations/20260501170646_AddWorkspaceApprovalConfiguration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddWorkspaceApprovalConfiguration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ApprovalMode",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "Required");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "LockContentAfterApproval",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "SchedulePostsAutomaticallyOnApproval",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "SendAutomaticApprovalReminders",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ApprovalMode",
|
||||||
|
table: "Workspaces");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LockContentAfterApproval",
|
||||||
|
table: "Workspaces");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SchedulePostsAutomaticallyOnApproval",
|
||||||
|
table: "Workspaces");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SendAutomaticApprovalReminders",
|
||||||
|
table: "Workspaces");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1361
backend/src/Socialize.Api/Migrations/20260501173710_AddWorkspaceApprovalStepConfiguration.Designer.cs
generated
Normal file
1361
backend/src/Socialize.Api/Migrations/20260501173710_AddWorkspaceApprovalStepConfiguration.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddWorkspaceApprovalStepConfiguration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "WorkspaceApprovalStepConfigurations",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TargetType = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
TargetValue = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
RequiredApproverCount = table.Column<int>(type: "integer", nullable: false, defaultValue: 1),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_WorkspaceApprovalStepConfigurations", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_WorkspaceApprovalStepConfigurations_WorkspaceId",
|
||||||
|
table: "WorkspaceApprovalStepConfigurations",
|
||||||
|
column: "WorkspaceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_WorkspaceApprovalStepConfigurations_WorkspaceId_SortOrder",
|
||||||
|
table: "WorkspaceApprovalStepConfigurations",
|
||||||
|
columns: new[] { "WorkspaceId", "SortOrder" },
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "WorkspaceApprovalStepConfigurations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1423
backend/src/Socialize.Api/Migrations/20260501175648_AddApprovalWorkflowRuntime.Designer.cs
generated
Normal file
1423
backend/src/Socialize.Api/Migrations/20260501175648_AddApprovalWorkflowRuntime.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddApprovalWorkflowRuntime : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "WorkflowInstanceId",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "WorkflowStepRequiredApproverCount",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "WorkflowStepSortOrder",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "WorkflowStepTargetType",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "WorkflowStepTargetValue",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
type: "character varying(128)",
|
||||||
|
maxLength: 128,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApprovalWorkflowInstances",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ContentItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
State = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
ApprovalMode = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
StartedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
CompletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApprovalWorkflowInstances", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalRequests_WorkflowInstanceId",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
column: "WorkflowInstanceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflowInstances_ContentItemId",
|
||||||
|
table: "ApprovalWorkflowInstances",
|
||||||
|
column: "ContentItemId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflowInstances_ContentItemId_State",
|
||||||
|
table: "ApprovalWorkflowInstances",
|
||||||
|
columns: new[] { "ContentItemId", "State" },
|
||||||
|
unique: true,
|
||||||
|
filter: "\"State\" = 'Pending'");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflowInstances_WorkspaceId",
|
||||||
|
table: "ApprovalWorkflowInstances",
|
||||||
|
column: "WorkspaceId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApprovalWorkflowInstances");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_ApprovalRequests_WorkflowInstanceId",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WorkflowInstanceId",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WorkflowStepRequiredApproverCount",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WorkflowStepSortOrder",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WorkflowStepTargetType",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "WorkflowStepTargetValue",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
using Socialize.Data;
|
using Socialize.Api.Data;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("AspNetUserTokens", (string)null);
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Approvals.Data.ApprovalDecision", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalDecision", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -168,7 +168,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("ApprovalDecisions", (string)null);
|
b.ToTable("ApprovalDecisions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Approvals.Data.ApprovalRequest", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalRequest", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -216,6 +216,23 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("WorkflowInstanceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int?>("WorkflowStepRequiredApproverCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("WorkflowStepSortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("WorkflowStepTargetType")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("WorkflowStepTargetValue")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
b.Property<Guid>("WorkspaceId")
|
b.Property<Guid>("WorkspaceId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -225,12 +242,104 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ReviewerEmail");
|
b.HasIndex("ReviewerEmail");
|
||||||
|
|
||||||
|
b.HasIndex("WorkflowInstanceId");
|
||||||
|
|
||||||
b.HasIndex("WorkspaceId");
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
b.ToTable("ApprovalRequests", (string)null);
|
b.ToTable("ApprovalRequests", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Assets.Data.Asset", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ApprovalMode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("CompletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("ContentItemId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("StartedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<Guid>("WorkspaceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ContentItemId");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
|
b.HasIndex("ContentItemId", "State")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("\"State\" = 'Pending'");
|
||||||
|
|
||||||
|
b.ToTable("ApprovalWorkflowInstances", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.WorkspaceApprovalStepConfiguration", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<int>("RequiredApproverCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(1);
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TargetType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("TargetValue")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<Guid>("WorkspaceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId", "SortOrder")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("WorkspaceApprovalStepConfigurations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -286,7 +395,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("Assets", (string)null);
|
b.ToTable("Assets", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Assets.Data.AssetRevision", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.AssetRevision", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -329,7 +438,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("AssetRevisions", (string)null);
|
b.ToTable("AssetRevisions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Clients.Data.Client", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -379,7 +488,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("Clients", (string)null);
|
b.ToTable("Clients", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Comments.Data.Comment", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Comments.Data.Comment", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -434,7 +543,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("Comments", (string)null);
|
b.ToTable("Comments", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.ContentItems.Data.ContentItem", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItem", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -500,7 +609,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("ContentItems", (string)null);
|
b.ToTable("ContentItems", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.ContentItems.Data.ContentItemRevision", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -558,7 +667,298 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("ContentItemRevisions", (string)null);
|
b.ToTable("ContentItemRevisions", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Identity.Data.Role", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ActivityType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("ActorDisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("ActorEmail")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid>("ActorUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("FeedbackReportId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("FromValue")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("Note")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b.Property<string>("ToValue")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ActorUserId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("FeedbackReportId");
|
||||||
|
|
||||||
|
b.ToTable("FeedbackActivityEntries", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackComment", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorDisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorEmail")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorRole")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<Guid>("AuthorUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Body")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(8000)
|
||||||
|
.HasColumnType("character varying(8000)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("FeedbackReportId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AuthorUserId");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedAt");
|
||||||
|
|
||||||
|
b.HasIndex("FeedbackReportId");
|
||||||
|
|
||||||
|
b.ToTable("FeedbackComments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AppVersion")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("BrowserUserAgent")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("CancellationReason")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("CancelledAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CancelledByUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ClientId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ClientName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ContentItemId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ContentItemTitle")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(8000)
|
||||||
|
.HasColumnType("character varying(8000)");
|
||||||
|
|
||||||
|
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)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("ReporterEmail")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid>("ReporterUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("SubmittedPath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<int?>("ViewportHeight")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("ViewportWidth")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid?>("WorkspaceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("WorkspaceName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("LastActivityAt");
|
||||||
|
|
||||||
|
b.HasIndex("ReporterUserId");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.HasIndex("Type");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
|
b.ToTable("FeedbackReports", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("BlobContainerName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("BlobName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("ContentType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("FeedbackReportId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("FileName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<long>("SizeBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("FeedbackReportId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("FeedbackScreenshots", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackTag", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("FeedbackReportId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName");
|
||||||
|
|
||||||
|
b.HasIndex("FeedbackReportId", "NormalizedName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("FeedbackTags", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Identity.Data.Role", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -585,7 +985,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("AspNetRoles", (string)null);
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Identity.Data.User", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Identity.Data.User", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -688,7 +1088,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("AspNetUsers", (string)null);
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Notifications.Data.NotificationEvent", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Notifications.Data.NotificationEvent", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -750,7 +1150,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("NotificationEvents", (string)null);
|
b.ToTable("NotificationEvents", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Projects.Data.Project", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Projects.Data.Project", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -803,17 +1203,33 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("Projects", (string)null);
|
b.ToTable("Projects", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Workspaces.Data.Workspace", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ApprovalMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasDefaultValue("Required");
|
||||||
|
|
||||||
b.Property<DateTimeOffset>("CreatedAt")
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<bool>("LockContentAfterApproval")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<string>("LogoUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
@@ -822,6 +1238,16 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Property<Guid>("OwnerUserId")
|
b.Property<Guid>("OwnerUserId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("SchedulePostsAutomaticallyOnApproval")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("SendAutomaticApprovalReminders")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
b.Property<string>("Slug")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
@@ -842,7 +1268,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("Workspaces", (string)null);
|
b.ToTable("Workspaces", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Modules.Workspaces.Data.WorkspaceInvite", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.WorkspaceInvite", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -885,7 +1311,7 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Modules.Identity.Data.Role", null)
|
b.HasOne("Socialize.Api.Modules.Identity.Data.Role", null)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("RoleId")
|
.HasForeignKey("RoleId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -894,7 +1320,7 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
b.HasOne("Socialize.Api.Modules.Identity.Data.User", null)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -903,7 +1329,7 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
b.HasOne("Socialize.Api.Modules.Identity.Data.User", null)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -912,13 +1338,13 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Modules.Identity.Data.Role", null)
|
b.HasOne("Socialize.Api.Modules.Identity.Data.Role", null)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("RoleId")
|
.HasForeignKey("RoleId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
|
|
||||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
b.HasOne("Socialize.Api.Modules.Identity.Data.User", null)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
@@ -927,12 +1353,67 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
b.HasOne("Socialize.Api.Modules.Identity.Data.User", null)
|
||||||
.WithMany()
|
.WithMany()
|
||||||
.HasForeignKey("UserId")
|
.HasForeignKey("UserId")
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||||
|
.WithMany("ActivityEntries")
|
||||||
|
.HasForeignKey("FeedbackReportId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("FeedbackReport");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackComment", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||||
|
.WithMany("Comments")
|
||||||
|
.HasForeignKey("FeedbackReportId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("FeedbackReport");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||||
|
.WithOne("Screenshot")
|
||||||
|
.HasForeignKey("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", "FeedbackReportId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("FeedbackReport");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackTag", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||||
|
.WithMany("Tags")
|
||||||
|
.HasForeignKey("FeedbackReportId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("FeedbackReport");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ActivityEntries");
|
||||||
|
|
||||||
|
b.Navigation("Comments");
|
||||||
|
|
||||||
|
b.Navigation("Screenshot");
|
||||||
|
|
||||||
|
b.Navigation("Tags");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Modules.Approvals.Data;
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public class ApprovalDecision
|
public class ApprovalDecision
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,33 @@
|
|||||||
namespace Socialize.Modules.Approvals.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public static class ApprovalModelConfiguration
|
public static class ApprovalModelConfiguration
|
||||||
{
|
{
|
||||||
public static ModelBuilder ConfigureApprovalsModule(this ModelBuilder modelBuilder)
|
public static ModelBuilder ConfigureApprovalsModule(this ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
modelBuilder.Entity<ApprovalWorkflowInstance>(workflowInstance =>
|
||||||
|
{
|
||||||
|
workflowInstance.ToTable("ApprovalWorkflowInstances");
|
||||||
|
workflowInstance.HasKey(x => x.Id);
|
||||||
|
workflowInstance.Property(x => x.State).HasMaxLength(64).IsRequired();
|
||||||
|
workflowInstance.Property(x => x.ApprovalMode).HasMaxLength(64).IsRequired();
|
||||||
|
workflowInstance.Property(x => x.StartedAt)
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
workflowInstance.HasIndex(x => x.WorkspaceId);
|
||||||
|
workflowInstance.HasIndex(x => x.ContentItemId);
|
||||||
|
workflowInstance.HasIndex(x => new { x.ContentItemId, x.State })
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("\"State\" = 'Pending'");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
|
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
|
||||||
{
|
{
|
||||||
approvalRequest.ToTable("ApprovalRequests");
|
approvalRequest.ToTable("ApprovalRequests");
|
||||||
approvalRequest.HasKey(x => x.Id);
|
approvalRequest.HasKey(x => x.Id);
|
||||||
|
approvalRequest.Property(x => x.WorkflowStepTargetType).HasMaxLength(32);
|
||||||
|
approvalRequest.Property(x => x.WorkflowStepTargetValue).HasMaxLength(128);
|
||||||
approvalRequest.Property(x => x.Stage).HasMaxLength(64).IsRequired();
|
approvalRequest.Property(x => x.Stage).HasMaxLength(64).IsRequired();
|
||||||
approvalRequest.Property(x => x.ReviewerName).HasMaxLength(256).IsRequired();
|
approvalRequest.Property(x => x.ReviewerName).HasMaxLength(256).IsRequired();
|
||||||
approvalRequest.Property(x => x.ReviewerEmail).HasMaxLength(256).IsRequired();
|
approvalRequest.Property(x => x.ReviewerEmail).HasMaxLength(256).IsRequired();
|
||||||
@@ -18,6 +38,7 @@ public static class ApprovalModelConfiguration
|
|||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
approvalRequest.HasIndex(x => x.WorkspaceId);
|
approvalRequest.HasIndex(x => x.WorkspaceId);
|
||||||
approvalRequest.HasIndex(x => x.ContentItemId);
|
approvalRequest.HasIndex(x => x.ContentItemId);
|
||||||
|
approvalRequest.HasIndex(x => x.WorkflowInstanceId);
|
||||||
approvalRequest.HasIndex(x => x.ReviewerEmail);
|
approvalRequest.HasIndex(x => x.ReviewerEmail);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -35,6 +56,21 @@ public static class ApprovalModelConfiguration
|
|||||||
approvalDecision.HasIndex(x => x.ApprovalRequestId);
|
approvalDecision.HasIndex(x => x.ApprovalRequestId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<WorkspaceApprovalStepConfiguration>(approvalStep =>
|
||||||
|
{
|
||||||
|
approvalStep.ToTable("WorkspaceApprovalStepConfigurations");
|
||||||
|
approvalStep.HasKey(x => x.Id);
|
||||||
|
approvalStep.Property(x => x.Name).HasMaxLength(128).IsRequired();
|
||||||
|
approvalStep.Property(x => x.TargetType).HasMaxLength(32).IsRequired();
|
||||||
|
approvalStep.Property(x => x.TargetValue).HasMaxLength(128).IsRequired();
|
||||||
|
approvalStep.Property(x => x.RequiredApproverCount).HasDefaultValue(1);
|
||||||
|
approvalStep.Property(x => x.CreatedAt)
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
approvalStep.HasIndex(x => x.WorkspaceId);
|
||||||
|
approvalStep.HasIndex(x => new { x.WorkspaceId, x.SortOrder }).IsUnique();
|
||||||
|
});
|
||||||
|
|
||||||
return modelBuilder;
|
return modelBuilder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
namespace Socialize.Modules.Approvals.Data;
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public class ApprovalRequest
|
public class ApprovalRequest
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
public Guid ContentItemId { get; set; }
|
public Guid ContentItemId { get; set; }
|
||||||
|
public Guid? WorkflowInstanceId { get; set; }
|
||||||
|
public int? WorkflowStepSortOrder { get; set; }
|
||||||
|
public string? WorkflowStepTargetType { get; set; }
|
||||||
|
public string? WorkflowStepTargetValue { get; set; }
|
||||||
|
public int? WorkflowStepRequiredApproverCount { get; set; }
|
||||||
public required string Stage { get; set; }
|
public required string Stage { get; set; }
|
||||||
public required string ReviewerName { get; set; }
|
public required string ReviewerName { get; set; }
|
||||||
public required string ReviewerEmail { get; set; }
|
public required string ReviewerEmail { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
|
public class ApprovalWorkflowInstance
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public Guid WorkspaceId { get; set; }
|
||||||
|
public Guid ContentItemId { get; set; }
|
||||||
|
public required string State { get; set; }
|
||||||
|
public required string ApprovalMode { get; set; }
|
||||||
|
public DateTimeOffset StartedAt { get; init; }
|
||||||
|
public DateTimeOffset? CompletedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
|
public class WorkspaceApprovalStepConfiguration
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public Guid WorkspaceId { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public required string TargetType { get; set; }
|
||||||
|
public required string TargetValue { get; set; }
|
||||||
|
public int RequiredApproverCount { get; set; } = 1;
|
||||||
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
using Socialize.Modules.Approvals.Data;
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
namespace Socialize.Modules.Approvals;
|
namespace Socialize.Api.Modules.Approvals;
|
||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder AddApprovalsModule(
|
public static WebApplicationBuilder AddApprovalsModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
|
builder.Services.AddScoped<ApprovalWorkflowRuntimeService>();
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Socialize.Infrastructure.Security;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Modules.Notifications.Contracts;
|
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.Modules.Approvals.Handlers;
|
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||||
|
|
||||||
public record CreateApprovalRequestRequest(
|
public record CreateApprovalRequestRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
@@ -39,7 +45,8 @@ public class CreateApprovalRequestHandler(
|
|||||||
|
|
||||||
public override async Task HandleAsync(CreateApprovalRequestRequest request, CancellationToken ct)
|
public override async Task HandleAsync(CreateApprovalRequestRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
ContentItem? contentItem = await dbContext.ContentItems
|
var contentItem = await dbContext
|
||||||
|
.ContentItems
|
||||||
.SingleOrDefaultAsync(
|
.SingleOrDefaultAsync(
|
||||||
candidate => candidate.Id == request.ContentItemId && candidate.WorkspaceId == request.WorkspaceId,
|
candidate => candidate.Id == request.ContentItemId && candidate.WorkspaceId == request.WorkspaceId,
|
||||||
ct);
|
ct);
|
||||||
@@ -57,7 +64,23 @@ public class CreateApprovalRequestHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApprovalRequest approval = new()
|
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(),
|
Id = Guid.NewGuid(),
|
||||||
WorkspaceId = request.WorkspaceId,
|
WorkspaceId = request.WorkspaceId,
|
||||||
@@ -74,14 +97,7 @@ public class CreateApprovalRequestHandler(
|
|||||||
|
|
||||||
dbContext.ApprovalRequests.Add(approval);
|
dbContext.ApprovalRequests.Add(approval);
|
||||||
|
|
||||||
if (approval.Stage == "Internal")
|
contentItem.Status = "In approval";
|
||||||
{
|
|
||||||
contentItem.Status = "In internal review";
|
|
||||||
}
|
|
||||||
else if (approval.Stage == "Client")
|
|
||||||
{
|
|
||||||
contentItem.Status = "In client review";
|
|
||||||
}
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(ct);
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
@@ -102,6 +118,11 @@ public class CreateApprovalRequestHandler(
|
|||||||
approval.Id,
|
approval.Id,
|
||||||
approval.WorkspaceId,
|
approval.WorkspaceId,
|
||||||
approval.ContentItemId,
|
approval.ContentItemId,
|
||||||
|
approval.WorkflowInstanceId,
|
||||||
|
approval.WorkflowStepSortOrder,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue,
|
||||||
|
approval.WorkflowStepRequiredApproverCount,
|
||||||
approval.Stage,
|
approval.Stage,
|
||||||
approval.ReviewerName,
|
approval.ReviewerName,
|
||||||
approval.ReviewerEmail,
|
approval.ReviewerEmail,
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
namespace Socialize.Modules.Approvals.Handlers;
|
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||||
|
|
||||||
public record GetApprovalsRequest(Guid ContentItemId);
|
public record GetApprovalsRequest(Guid ContentItemId);
|
||||||
|
|
||||||
@@ -19,6 +24,11 @@ public record ApprovalRequestDto(
|
|||||||
Guid Id,
|
Guid Id,
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ContentItemId,
|
Guid ContentItemId,
|
||||||
|
Guid? WorkflowInstanceId,
|
||||||
|
int? WorkflowStepSortOrder,
|
||||||
|
string? WorkflowStepTargetType,
|
||||||
|
string? WorkflowStepTargetValue,
|
||||||
|
int? WorkflowStepRequiredApproverCount,
|
||||||
string Stage,
|
string Stage,
|
||||||
string ReviewerName,
|
string ReviewerName,
|
||||||
string ReviewerEmail,
|
string ReviewerEmail,
|
||||||
@@ -60,6 +70,7 @@ public class GetApprovalsHandler(
|
|||||||
List<ApprovalRequest> approvals = await dbContext.ApprovalRequests
|
List<ApprovalRequest> approvals = await dbContext.ApprovalRequests
|
||||||
.Where(approval => approval.ContentItemId == request.ContentItemId)
|
.Where(approval => approval.ContentItemId == request.ContentItemId)
|
||||||
.OrderByDescending(approval => approval.SentAt)
|
.OrderByDescending(approval => approval.SentAt)
|
||||||
|
.ThenBy(approval => approval.WorkflowStepSortOrder)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
List<Guid> approvalIds = approvals
|
List<Guid> approvalIds = approvals
|
||||||
@@ -86,6 +97,11 @@ public class GetApprovalsHandler(
|
|||||||
approval.Id,
|
approval.Id,
|
||||||
approval.WorkspaceId,
|
approval.WorkspaceId,
|
||||||
approval.ContentItemId,
|
approval.ContentItemId,
|
||||||
|
approval.WorkflowInstanceId,
|
||||||
|
approval.WorkflowStepSortOrder,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue,
|
||||||
|
approval.WorkflowStepRequiredApproverCount,
|
||||||
approval.Stage,
|
approval.Stage,
|
||||||
approval.ReviewerName,
|
approval.ReviewerName,
|
||||||
approval.ReviewerEmail,
|
approval.ReviewerEmail,
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.Notifications.Contracts;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
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.Modules.Approvals.Handlers;
|
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||||
|
|
||||||
public record SubmitApprovalDecisionRequest(
|
public record SubmitApprovalDecisionRequest(
|
||||||
string Decision,
|
string Decision,
|
||||||
@@ -14,7 +21,10 @@ public class SubmitApprovalDecisionRequestValidator
|
|||||||
{
|
{
|
||||||
public SubmitApprovalDecisionRequestValidator()
|
public SubmitApprovalDecisionRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.Decision).NotEmpty().MaximumLength(64);
|
RuleFor(x => x.Decision)
|
||||||
|
.NotEmpty()
|
||||||
|
.Equal("Approved")
|
||||||
|
.WithMessage("Only approved decisions are supported.");
|
||||||
RuleFor(x => x.Comment).MaximumLength(2048);
|
RuleFor(x => x.Comment).MaximumLength(2048);
|
||||||
RuleFor(x => x.ReviewerName).MaximumLength(256);
|
RuleFor(x => x.ReviewerName).MaximumLength(256);
|
||||||
RuleFor(x => x.ReviewerEmail).MaximumLength(256).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ReviewerEmail));
|
RuleFor(x => x.ReviewerEmail).MaximumLength(256).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ReviewerEmail));
|
||||||
@@ -24,6 +34,7 @@ public class SubmitApprovalDecisionRequestValidator
|
|||||||
public class SubmitApprovalDecisionHandler(
|
public class SubmitApprovalDecisionHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
|
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<SubmitApprovalDecisionRequest, ApprovalRequestDto>
|
: Endpoint<SubmitApprovalDecisionRequest, ApprovalRequestDto>
|
||||||
{
|
{
|
||||||
@@ -59,6 +70,13 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Workspace? workspace = await dbContext.Workspaces.SingleOrDefaultAsync(candidate => candidate.Id == contentItem.WorkspaceId, ct);
|
||||||
|
if (workspace is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string normalizedDecision = request.Decision.Trim();
|
string normalizedDecision = request.Decision.Trim();
|
||||||
string decidedByName = User?.Identity?.IsAuthenticated == true
|
string decidedByName = User?.Identity?.IsAuthenticated == true
|
||||||
? User.GetAlias() ?? User.GetName()
|
? User.GetAlias() ?? User.GetName()
|
||||||
@@ -79,45 +97,44 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
|
|
||||||
approval.State = normalizedDecision;
|
ApprovalWorkflowDecisionResult workflowDecisionResult = await approvalWorkflowRuntimeService
|
||||||
approval.CompletedAt = DateTimeOffset.UtcNow;
|
.ApplyWorkflowStepDecisionAsync(approval, contentItem, workspace, User!, decision, ct);
|
||||||
|
|
||||||
if (approval.Stage == "Internal")
|
if (!workflowDecisionResult.Succeeded)
|
||||||
{
|
{
|
||||||
contentItem.Status = normalizedDecision switch
|
AddError(request => request.Decision, workflowDecisionResult.ErrorMessage ?? "The approval decision could not be recorded.");
|
||||||
{
|
await SendErrorsAsync(workflowDecisionResult.StatusCode, ct);
|
||||||
"Approved" => "Ready for client review",
|
return;
|
||||||
"Changes requested" => "Changes requested internally",
|
|
||||||
"Rejected" => "Rejected",
|
|
||||||
_ => contentItem.Status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (approval.Stage == "Client")
|
|
||||||
{
|
|
||||||
contentItem.Status = normalizedDecision switch
|
|
||||||
{
|
|
||||||
"Approved" => "Approved",
|
|
||||||
"Changes requested" => "Changes requested by client",
|
|
||||||
"Rejected" => "Rejected",
|
|
||||||
_ => contentItem.Status,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.ApprovalDecisions.Add(decision);
|
if (!workflowDecisionResult.IsWorkflowStep)
|
||||||
await dbContext.SaveChangesAsync(ct);
|
{
|
||||||
|
approval.State = normalizedDecision;
|
||||||
|
approval.CompletedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
await notificationEventWriter.WriteAsync(
|
if (normalizedDecision == "Approved")
|
||||||
new NotificationEventWriteModel(
|
{
|
||||||
approval.WorkspaceId,
|
contentItem.Status = ApprovalWorkflowRules.GetFinalApprovalStatus(
|
||||||
approval.ContentItemId,
|
workspace.SchedulePostsAutomaticallyOnApproval,
|
||||||
"approval.decision.recorded",
|
contentItem.DueDate);
|
||||||
"ApprovalDecision",
|
}
|
||||||
decision.Id,
|
|
||||||
$"{decidedByName} recorded {normalizedDecision} for {contentItem.Title}.",
|
dbContext.ApprovalDecisions.Add(decision);
|
||||||
null,
|
await dbContext.SaveChangesAsync(ct);
|
||||||
decidedByEmail,
|
|
||||||
$$"""{"stage":"{{approval.Stage}}","status":"{{contentItem.Status}}"}"""),
|
await notificationEventWriter.WriteAsync(
|
||||||
ct);
|
new NotificationEventWriteModel(
|
||||||
|
approval.WorkspaceId,
|
||||||
|
approval.ContentItemId,
|
||||||
|
"approval.decision.recorded",
|
||||||
|
"ApprovalDecision",
|
||||||
|
decision.Id,
|
||||||
|
$"{decidedByName} recorded {normalizedDecision} for {contentItem.Title}.",
|
||||||
|
null,
|
||||||
|
decidedByEmail,
|
||||||
|
$$"""{"stage":"{{approval.Stage}}","status":"{{contentItem.Status}}"}"""),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
List<ApprovalDecision> decisions = await dbContext.ApprovalDecisions
|
List<ApprovalDecision> decisions = await dbContext.ApprovalDecisions
|
||||||
.Where(candidate => candidate.ApprovalRequestId == approval.Id)
|
.Where(candidate => candidate.ApprovalRequestId == approval.Id)
|
||||||
@@ -153,6 +170,11 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
approval.Id,
|
approval.Id,
|
||||||
approval.WorkspaceId,
|
approval.WorkspaceId,
|
||||||
approval.ContentItemId,
|
approval.ContentItemId,
|
||||||
|
approval.WorkflowInstanceId,
|
||||||
|
approval.WorkflowStepSortOrder,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue,
|
||||||
|
approval.WorkflowStepRequiredApproverCount,
|
||||||
approval.Stage,
|
approval.Stage,
|
||||||
approval.ReviewerName,
|
approval.ReviewerName,
|
||||||
approval.ReviewerEmail,
|
approval.ReviewerEmail,
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
|
public static class ApprovalStepTargetTypes
|
||||||
|
{
|
||||||
|
public const string Role = "Role";
|
||||||
|
public const string Membership = "Membership";
|
||||||
|
public const string Member = "Member";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ApprovalMembershipTargets
|
||||||
|
{
|
||||||
|
public const string Team = "Team";
|
||||||
|
public const string Client = "Client";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ApprovalStepConfigurationRules
|
||||||
|
{
|
||||||
|
public static readonly IReadOnlySet<string> AllowedTargetTypes = new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Role,
|
||||||
|
ApprovalStepTargetTypes.Membership,
|
||||||
|
ApprovalStepTargetTypes.Member,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly IReadOnlySet<string> AllowedRoleTargets = new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
KnownRoles.Administrator,
|
||||||
|
KnownRoles.Manager,
|
||||||
|
KnownRoles.WorkspaceMember,
|
||||||
|
KnownRoles.Client,
|
||||||
|
KnownRoles.Provider,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly IReadOnlySet<string> AllowedMembershipTargets = new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
ApprovalMembershipTargets.Team,
|
||||||
|
ApprovalMembershipTargets.Client,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool IsValidTargetType(string? targetType)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(targetType) && AllowedTargetTypes.Contains(targetType.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidRoleTarget(string? targetValue)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(targetValue) && AllowedRoleTargets.Contains(targetValue.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidMembershipTarget(string? targetValue)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(targetValue) && AllowedMembershipTargets.Contains(targetValue.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
|
public static class ApprovalModes
|
||||||
|
{
|
||||||
|
public const string None = "None";
|
||||||
|
public const string Optional = "Optional";
|
||||||
|
public const string Required = "Required";
|
||||||
|
public const string MultiLevel = "Multi-level";
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsApprovalCompletionStatus(string status)
|
||||||
|
{
|
||||||
|
return status is "Approved" or "Scheduled";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetFinalApprovalStatus(bool schedulePostsAutomaticallyOnApproval, DateTimeOffset? plannedPublishDate)
|
||||||
|
{
|
||||||
|
return schedulePostsAutomaticallyOnApproval && plannedPublishDate.HasValue
|
||||||
|
? "Scheduled"
|
||||||
|
: "Approved";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasRequiredStepApprovals(int approvedDecisionCount, int requiredApproverCount)
|
||||||
|
{
|
||||||
|
return approvedDecisionCount >= Math.Max(1, requiredApproverCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CanApproveWorkflowStep(
|
||||||
|
bool isAdministrator,
|
||||||
|
bool hasWorkspaceAccess,
|
||||||
|
IReadOnlyCollection<string> userRoles,
|
||||||
|
Guid userId,
|
||||||
|
string? targetType,
|
||||||
|
string? targetValue)
|
||||||
|
{
|
||||||
|
if (isAdministrator)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasWorkspaceAccess ||
|
||||||
|
string.IsNullOrWhiteSpace(targetType) ||
|
||||||
|
string.IsNullOrWhiteSpace(targetValue))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetType switch
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Role => userRoles.Contains(targetValue),
|
||||||
|
ApprovalStepTargetTypes.Membership => MatchesMembershipTarget(userRoles, targetValue),
|
||||||
|
ApprovalStepTargetTypes.Member => ParseMemberTargetIds(targetValue).Contains(userId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyCollection<Guid> ParseMemberTargetIds(string? targetValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(targetValue))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetValue
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(value => Guid.TryParse(value, out Guid memberUserId) ? memberUserId : Guid.Empty)
|
||||||
|
.Where(memberUserId => memberUserId != Guid.Empty)
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatMemberTargetValue(IEnumerable<Guid> memberUserIds)
|
||||||
|
{
|
||||||
|
return string.Join(",", memberUserIds.Distinct().OrderBy(memberUserId => memberUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesMembershipTarget(
|
||||||
|
IReadOnlyCollection<string> userRoles,
|
||||||
|
string targetValue)
|
||||||
|
{
|
||||||
|
return targetValue switch
|
||||||
|
{
|
||||||
|
ApprovalMembershipTargets.Client => userRoles.Contains(KnownRoles.Client),
|
||||||
|
ApprovalMembershipTargets.Team => !userRoles.Contains(KnownRoles.Client),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,401 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
|
public record ApprovalWorkflowStartResult(bool Succeeded, string? ErrorMessage);
|
||||||
|
|
||||||
|
public record ApprovalWorkflowDecisionResult(
|
||||||
|
bool Succeeded,
|
||||||
|
string? ErrorMessage,
|
||||||
|
int StatusCode,
|
||||||
|
bool IsWorkflowStep);
|
||||||
|
|
||||||
|
public class ApprovalWorkflowRuntimeService(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
INotificationEventWriter notificationEventWriter)
|
||||||
|
{
|
||||||
|
private const string PendingState = "Pending";
|
||||||
|
private const string ApprovedState = "Approved";
|
||||||
|
|
||||||
|
public async Task<ApprovalWorkflowStartResult> StartMultiLevelWorkflowAsync(
|
||||||
|
ContentItem contentItem,
|
||||||
|
Workspace workspace,
|
||||||
|
Guid requestedByUserId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (workspace.ApprovalMode != ApprovalModes.MultiLevel)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowStartResult(false, "The workspace is not configured for multi-level approval.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ApprovalWorkflowInstance? activeWorkflow = await dbContext.ApprovalWorkflowInstances
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
workflow => workflow.ContentItemId == contentItem.Id && workflow.State == PendingState,
|
||||||
|
ct);
|
||||||
|
if (activeWorkflow is not null)
|
||||||
|
{
|
||||||
|
contentItem.Status = "In approval";
|
||||||
|
return new ApprovalWorkflowStartResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WorkspaceApprovalStepConfiguration> configuredSteps = await dbContext.WorkspaceApprovalStepConfigurations
|
||||||
|
.Where(step => step.WorkspaceId == workspace.Id)
|
||||||
|
.OrderBy(step => step.SortOrder)
|
||||||
|
.ThenBy(step => step.Name)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
if (configuredSteps.Count == 0)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowStartResult(false, "Multi-level approval requires at least one configured approval step.");
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
var workflowInstance = new ApprovalWorkflowInstance
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
WorkspaceId = workspace.Id,
|
||||||
|
ContentItemId = contentItem.Id,
|
||||||
|
State = PendingState,
|
||||||
|
ApprovalMode = workspace.ApprovalMode,
|
||||||
|
StartedAt = now,
|
||||||
|
};
|
||||||
|
|
||||||
|
List<ApprovalRequest> workflowSteps = configuredSteps
|
||||||
|
.Select((step, index) => new ApprovalRequest
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
WorkspaceId = workspace.Id,
|
||||||
|
ContentItemId = contentItem.Id,
|
||||||
|
WorkflowInstanceId = workflowInstance.Id,
|
||||||
|
WorkflowStepSortOrder = index,
|
||||||
|
WorkflowStepTargetType = step.TargetType,
|
||||||
|
WorkflowStepTargetValue = step.TargetValue,
|
||||||
|
WorkflowStepRequiredApproverCount = step.RequiredApproverCount,
|
||||||
|
Stage = step.Name,
|
||||||
|
ReviewerName = FormatStepTarget(step),
|
||||||
|
ReviewerEmail = string.Empty,
|
||||||
|
RequestedByUserId = requestedByUserId,
|
||||||
|
DueAt = contentItem.DueDate,
|
||||||
|
State = PendingState,
|
||||||
|
AccessToken = CreateAccessToken(),
|
||||||
|
SentAt = now,
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
dbContext.ApprovalWorkflowInstances.Add(workflowInstance);
|
||||||
|
dbContext.ApprovalRequests.AddRange(workflowSteps);
|
||||||
|
contentItem.Status = "In approval";
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
await NotifyCurrentStepApproversAsync(workflowSteps[0], contentItem, ct);
|
||||||
|
|
||||||
|
return new ApprovalWorkflowStartResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApprovalWorkflowDecisionResult> ApplyWorkflowStepDecisionAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
ContentItem contentItem,
|
||||||
|
Workspace workspace,
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
ApprovalDecision decision,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!approval.WorkflowInstanceId.HasValue)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(true, null, StatusCodes.Status200OK, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Identity?.IsAuthenticated != true)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "Multi-level approval steps require an authenticated approver.", StatusCodes.Status401Unauthorized, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await CanApproveStepAsync(user, approval, workspace.Id, ct))
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "You cannot approve the current workflow step.", StatusCodes.Status403Forbidden, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApprovalRequest? currentStep = await GetCurrentPendingStepAsync(approval.WorkflowInstanceId.Value, ct);
|
||||||
|
if (currentStep?.Id != approval.Id)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "Only the current pending approval step can be approved.", StatusCodes.Status409Conflict, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid currentUserId = user.GetUserId();
|
||||||
|
bool alreadyApproved = await dbContext.ApprovalDecisions.AnyAsync(
|
||||||
|
candidate => candidate.ApprovalRequestId == approval.Id &&
|
||||||
|
candidate.DecidedByUserId == currentUserId &&
|
||||||
|
candidate.Decision == ApprovedState,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (alreadyApproved)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "You have already approved this workflow step.", StatusCodes.Status409Conflict, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.ApprovalDecisions.Add(decision);
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
int approvedCount = await dbContext.ApprovalDecisions
|
||||||
|
.Where(candidate => candidate.ApprovalRequestId == approval.Id && candidate.Decision == ApprovedState)
|
||||||
|
.Select(candidate => candidate.DecidedByUserId.HasValue
|
||||||
|
? candidate.DecidedByUserId.Value.ToString()
|
||||||
|
: candidate.DecidedByEmail.ToLower())
|
||||||
|
.Distinct()
|
||||||
|
.CountAsync(ct);
|
||||||
|
|
||||||
|
int requiredApproverCount = approval.WorkflowStepRequiredApproverCount ?? 1;
|
||||||
|
if (!ApprovalWorkflowRules.HasRequiredStepApprovals(approvedCount, requiredApproverCount))
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(true, null, StatusCodes.Status200OK, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
approval.State = ApprovedState;
|
||||||
|
approval.CompletedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
ApprovalRequest? nextStep = await dbContext.ApprovalRequests
|
||||||
|
.Where(candidate => candidate.WorkflowInstanceId == approval.WorkflowInstanceId &&
|
||||||
|
candidate.State == PendingState &&
|
||||||
|
candidate.Id != approval.Id)
|
||||||
|
.OrderBy(candidate => candidate.WorkflowStepSortOrder)
|
||||||
|
.ThenBy(candidate => candidate.SentAt)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
|
if (nextStep is null)
|
||||||
|
{
|
||||||
|
ApprovalWorkflowInstance? workflowInstance = await dbContext.ApprovalWorkflowInstances
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == approval.WorkflowInstanceId.Value, ct);
|
||||||
|
if (workflowInstance is null)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "The approval workflow instance could not be found.", StatusCodes.Status404NotFound, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowInstance.State = ApprovedState;
|
||||||
|
workflowInstance.CompletedAt = DateTimeOffset.UtcNow;
|
||||||
|
contentItem.Status = ApprovalWorkflowRules.GetFinalApprovalStatus(
|
||||||
|
workspace.SchedulePostsAutomaticallyOnApproval,
|
||||||
|
contentItem.DueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
if (nextStep is null)
|
||||||
|
{
|
||||||
|
await NotifyPublishUsersAsync(approval, contentItem, ct);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await NotifyCurrentStepApproversAsync(nextStep, contentItem, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ApprovalWorkflowDecisionResult(true, null, StatusCodes.Status200OK, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasCompletedMultiLevelWorkflowAsync(Guid contentItemId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await dbContext.ApprovalWorkflowInstances.AnyAsync(
|
||||||
|
workflow => workflow.ContentItemId == contentItemId && workflow.State == ApprovedState,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ApprovalRequest?> GetCurrentPendingStepAsync(Guid workflowInstanceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await dbContext.ApprovalRequests
|
||||||
|
.Where(candidate => candidate.WorkflowInstanceId == workflowInstanceId && candidate.State == PendingState)
|
||||||
|
.OrderBy(candidate => candidate.WorkflowStepSortOrder)
|
||||||
|
.ThenBy(candidate => candidate.SentAt)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CanApproveStepAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
ApprovalRequest approval,
|
||||||
|
Guid workspaceId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
Guid userId = user.GetUserId();
|
||||||
|
bool hasWorkspaceAccess = await UserHasWorkspaceAccessAsync(userId, workspaceId, ct);
|
||||||
|
string[] userRoles = ApprovalStepConfigurationRules.AllowedRoleTargets
|
||||||
|
.Where(user.IsInRole)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return ApprovalWorkflowRules.CanApproveWorkflowStep(
|
||||||
|
user.IsInRole(KnownRoles.Administrator),
|
||||||
|
hasWorkspaceAccess,
|
||||||
|
userRoles,
|
||||||
|
userId,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> UserHasWorkspaceAccessAsync(Guid userId, Guid workspaceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
string workspaceClaimValue = workspaceId.ToString();
|
||||||
|
return await dbContext.UserClaims.AnyAsync(
|
||||||
|
claim => claim.UserId == userId &&
|
||||||
|
claim.ClaimType == KnownClaims.WorkspaceScope &&
|
||||||
|
claim.ClaimValue == workspaceClaimValue,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NotifyCurrentStepApproversAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
ContentItem contentItem,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
List<ApprovalNotificationRecipient> recipients = await GetStepApproverRecipientsAsync(approval, ct);
|
||||||
|
|
||||||
|
foreach (ApprovalNotificationRecipient recipient in recipients)
|
||||||
|
{
|
||||||
|
await notificationEventWriter.WriteAsync(
|
||||||
|
new NotificationEventWriteModel(
|
||||||
|
approval.WorkspaceId,
|
||||||
|
approval.ContentItemId,
|
||||||
|
"approval.step.current",
|
||||||
|
"ApprovalRequest",
|
||||||
|
approval.Id,
|
||||||
|
$"{approval.Stage} approval is ready for {contentItem.Title}.",
|
||||||
|
recipient.UserId,
|
||||||
|
recipient.Email,
|
||||||
|
$$"""{"stage":"{{approval.Stage}}","requiredApproverCount":{{approval.WorkflowStepRequiredApproverCount ?? 1}}}"""),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NotifyPublishUsersAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
ContentItem contentItem,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
List<ApprovalNotificationRecipient> recipients = await GetPublishRecipientUsersAsync(approval.WorkspaceId, ct);
|
||||||
|
|
||||||
|
foreach (ApprovalNotificationRecipient recipient in recipients)
|
||||||
|
{
|
||||||
|
await notificationEventWriter.WriteAsync(
|
||||||
|
new NotificationEventWriteModel(
|
||||||
|
approval.WorkspaceId,
|
||||||
|
approval.ContentItemId,
|
||||||
|
"approval.workflow.completed",
|
||||||
|
"ApprovalWorkflowInstance",
|
||||||
|
approval.WorkflowInstanceId!.Value,
|
||||||
|
$"Final approval completed for {contentItem.Title}.",
|
||||||
|
recipient.UserId,
|
||||||
|
recipient.Email,
|
||||||
|
$$"""{"status":"{{contentItem.Status}}"}"""),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetStepApproverRecipientsAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
string? targetType = approval.WorkflowStepTargetType;
|
||||||
|
string? targetValue = approval.WorkflowStepTargetValue;
|
||||||
|
if (string.IsNullOrWhiteSpace(targetType) || string.IsNullOrWhiteSpace(targetValue))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetType switch
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Member => await GetMemberRecipientsAsync(targetValue, ct),
|
||||||
|
ApprovalStepTargetTypes.Role => await GetRoleRecipientsAsync(approval.WorkspaceId, [targetValue], ct),
|
||||||
|
ApprovalStepTargetTypes.Membership => await GetMembershipRecipientsAsync(approval.WorkspaceId, targetValue, ct),
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetMemberRecipientsAsync(string targetValue, CancellationToken ct)
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<Guid> userIds = ApprovalWorkflowRules.ParseMemberTargetIds(targetValue);
|
||||||
|
if (userIds.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await dbContext.Users
|
||||||
|
.Where(user => userIds.Contains(user.Id))
|
||||||
|
.Select(user => new ApprovalNotificationRecipient(user.Id, user.Email))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetMembershipRecipientsAsync(
|
||||||
|
Guid workspaceId,
|
||||||
|
string targetValue,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
string[] roles = targetValue switch
|
||||||
|
{
|
||||||
|
ApprovalMembershipTargets.Client => [KnownRoles.Client],
|
||||||
|
ApprovalMembershipTargets.Team => [KnownRoles.Administrator, KnownRoles.Manager, KnownRoles.WorkspaceMember, KnownRoles.Provider],
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return roles.Length == 0
|
||||||
|
? []
|
||||||
|
: await GetRoleRecipientsAsync(workspaceId, roles, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetPublishRecipientUsersAsync(Guid workspaceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await GetRoleRecipientsAsync(workspaceId, [KnownRoles.Administrator, KnownRoles.Manager], ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetRoleRecipientsAsync(
|
||||||
|
Guid workspaceId,
|
||||||
|
IReadOnlyCollection<string> roles,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
string workspaceClaimValue = workspaceId.ToString();
|
||||||
|
|
||||||
|
return await dbContext.UserRoles
|
||||||
|
.Join(
|
||||||
|
dbContext.Roles,
|
||||||
|
userRole => userRole.RoleId,
|
||||||
|
role => role.Id,
|
||||||
|
(userRole, role) => new { userRole.UserId, RoleName = role.Name })
|
||||||
|
.Where(candidate => candidate.RoleName != null && roles.Contains(candidate.RoleName))
|
||||||
|
.Join(
|
||||||
|
dbContext.UserClaims.Where(claim =>
|
||||||
|
claim.ClaimType == KnownClaims.WorkspaceScope &&
|
||||||
|
claim.ClaimValue == workspaceClaimValue),
|
||||||
|
candidate => candidate.UserId,
|
||||||
|
claim => claim.UserId,
|
||||||
|
(candidate, _) => candidate.UserId)
|
||||||
|
.Distinct()
|
||||||
|
.Join(
|
||||||
|
dbContext.Users,
|
||||||
|
userId => userId,
|
||||||
|
user => user.Id,
|
||||||
|
(_, user) => new ApprovalNotificationRecipient(user.Id, user.Email))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatStepTarget(WorkspaceApprovalStepConfiguration step)
|
||||||
|
{
|
||||||
|
return step.TargetType switch
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Role => $"Role: {step.TargetValue}",
|
||||||
|
ApprovalStepTargetTypes.Membership => $"Membership: {step.TargetValue}",
|
||||||
|
ApprovalStepTargetTypes.Member => "Assigned members",
|
||||||
|
_ => step.TargetValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateAccessToken()
|
||||||
|
{
|
||||||
|
return Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record ApprovalNotificationRecipient(Guid UserId, string? Email);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Modules.Assets.Data;
|
namespace Socialize.Api.Modules.Assets.Data;
|
||||||
|
|
||||||
public class Asset
|
public class Asset
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Socialize.Modules.Assets.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Assets.Data;
|
||||||
|
|
||||||
public static class AssetModelConfiguration
|
public static class AssetModelConfiguration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Modules.Assets.Data;
|
namespace Socialize.Api.Modules.Assets.Data;
|
||||||
|
|
||||||
public class AssetRevision
|
public class AssetRevision
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Socialize.Modules.Assets.Data;
|
using Socialize.Api.Modules.Assets.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.Assets;
|
namespace Socialize.Api.Modules.Assets;
|
||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.Notifications.Contracts;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Assets.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
|
||||||
namespace Socialize.Modules.Assets.Handlers;
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
public record CreateAssetRevisionRequest(
|
public record CreateAssetRevisionRequest(
|
||||||
string SourceReference,
|
string SourceReference,
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.Notifications.Contracts;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Assets.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
|
||||||
namespace Socialize.Modules.Assets.Handlers;
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
public record CreateGoogleDriveAssetRequest(
|
public record CreateGoogleDriveAssetRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
namespace Socialize.Modules.Assets.Handlers;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
public record GetAssetsRequest(Guid ContentItemId);
|
public record GetAssetsRequest(Guid ContentItemId);
|
||||||
|
|
||||||
@@ -40,7 +44,7 @@ public class GetAssetsHandler(
|
|||||||
|
|
||||||
public override async Task HandleAsync(GetAssetsRequest request, CancellationToken ct)
|
public override async Task HandleAsync(GetAssetsRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
ContentItem? item = await dbContext.ContentItems
|
var item = await dbContext.ContentItems
|
||||||
.SingleOrDefaultAsync(candidate => candidate.Id == request.ContentItemId, ct);
|
.SingleOrDefaultAsync(candidate => candidate.Id == request.ContentItemId, ct);
|
||||||
if (item is null)
|
if (item is null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Modules.Clients.Data;
|
namespace Socialize.Api.Modules.Clients.Data;
|
||||||
|
|
||||||
public class Client
|
public class Client
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Socialize.Modules.Clients.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Clients.Data;
|
||||||
|
|
||||||
public static class ClientModelConfiguration
|
public static class ClientModelConfiguration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Socialize.Modules.Clients.Data;
|
using Socialize.Api.Modules.Clients.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.Clients;
|
namespace Socialize.Api.Modules.Clients;
|
||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
using Socialize.Infrastructure.BlobStorage.Contracts;
|
using FastEndpoints;
|
||||||
using Socialize.Infrastructure.Security;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Modules.Clients.Data;
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Clients.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.Clients.Handlers;
|
namespace Socialize.Api.Modules.Clients.Handlers;
|
||||||
|
|
||||||
public record ChangeClientPortraitRequest(
|
public record ChangeClientPortraitRequest(
|
||||||
IFormFile File);
|
IFormFile File);
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
namespace Socialize.Modules.Clients.Handlers;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Modules.Clients.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Clients.Handlers;
|
||||||
|
|
||||||
public record CreateClientRequest(
|
public record CreateClientRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.Clients.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Clients.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.Clients.Handlers;
|
namespace Socialize.Api.Modules.Clients.Handlers;
|
||||||
|
|
||||||
public record GetClientsRequest(Guid? WorkspaceId);
|
public record GetClientsRequest(Guid? WorkspaceId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.Clients.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Clients.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.Clients.Handlers;
|
namespace Socialize.Api.Modules.Clients.Handlers;
|
||||||
|
|
||||||
public record UpdateClientRequest(
|
public record UpdateClientRequest(
|
||||||
string Name,
|
string Name,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Modules.Comments.Data;
|
namespace Socialize.Api.Modules.Comments.Data;
|
||||||
|
|
||||||
public class Comment
|
public class Comment
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Socialize.Modules.Comments.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Comments.Data;
|
||||||
|
|
||||||
public static class CommentModelConfiguration
|
public static class CommentModelConfiguration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Socialize.Modules.Comments.Data;
|
using Socialize.Api.Modules.Comments.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.Comments;
|
namespace Socialize.Api.Modules.Comments;
|
||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.Notifications.Contracts;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Comments.Data;
|
||||||
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
|
||||||
namespace Socialize.Modules.Comments.Handlers;
|
namespace Socialize.Api.Modules.Comments.Handlers;
|
||||||
|
|
||||||
public record CreateCommentRequest(
|
public record CreateCommentRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Modules.Comments.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
namespace Socialize.Modules.Comments.Handlers;
|
namespace Socialize.Api.Modules.Comments.Handlers;
|
||||||
|
|
||||||
public record GetCommentsRequest(Guid ContentItemId);
|
public record GetCommentsRequest(Guid ContentItemId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.Notifications.Contracts;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Comments.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
|
||||||
namespace Socialize.Modules.Comments.Handlers;
|
namespace Socialize.Api.Modules.Comments.Handlers;
|
||||||
|
|
||||||
public class ResolveCommentHandler(
|
public class ResolveCommentHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Modules.ContentItems.Data;
|
namespace Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
|
||||||
public class ContentItem
|
public class ContentItem
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace Socialize.Modules.ContentItems.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
|
||||||
public static class ContentItemModelConfiguration
|
public static class ContentItemModelConfiguration
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace Socialize.Modules.ContentItems.Data;
|
namespace Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
|
||||||
public class ContentItemRevision
|
public class ContentItemRevision
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Socialize.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.ContentItems;
|
namespace Socialize.Api.Modules.ContentItems;
|
||||||
|
|
||||||
public static class DependencyInjection
|
public static class DependencyInjection
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.Notifications.Contracts;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.ContentItems.Handlers;
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
public record CreateContentItemRequest(
|
public record CreateContentItemRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.Notifications.Contracts;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
|
||||||
namespace Socialize.Modules.ContentItems.Handlers;
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
public record CreateContentItemRevisionRequest(
|
public record CreateContentItemRevisionRequest(
|
||||||
string Title,
|
string Title,
|
||||||
@@ -62,15 +66,6 @@ public class CreateContentItemRevisionHandler(
|
|||||||
item.CurrentRevisionNumber = revisionNumber;
|
item.CurrentRevisionNumber = revisionNumber;
|
||||||
item.CurrentRevisionLabel = revisionLabel;
|
item.CurrentRevisionLabel = revisionLabel;
|
||||||
|
|
||||||
if (item.Status == "Changes requested internally")
|
|
||||||
{
|
|
||||||
item.Status = "Internal changes in progress";
|
|
||||||
}
|
|
||||||
else if (item.Status == "Changes requested by client")
|
|
||||||
{
|
|
||||||
item.Status = "Client changes in progress";
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentItemRevision revision = new()
|
ContentItemRevision revision = new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.ContentItems.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.ContentItems.Handlers;
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
public record ContentItemDetailDto(
|
public record ContentItemDetailDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
namespace Socialize.Modules.ContentItems.Handlers;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
public record ContentItemRevisionDto(
|
public record ContentItemRevisionDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.ContentItems.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.ContentItems.Handlers;
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
public record GetContentItemsRequest(Guid? WorkspaceId, Guid? ClientId, Guid? ProjectId);
|
public record GetContentItemsRequest(Guid? WorkspaceId, Guid? ClientId, Guid? ProjectId);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
using Socialize.Infrastructure.Security;
|
using FastEndpoints;
|
||||||
using Socialize.Modules.ContentItems.Data;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Modules.Notifications.Contracts;
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Modules.ContentItems.Handlers;
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
public record UpdateContentItemStatusRequest(string Status);
|
public record UpdateContentItemStatusRequest(string Status);
|
||||||
|
|
||||||
@@ -18,24 +23,18 @@ public class UpdateContentItemStatusRequestValidator
|
|||||||
public class UpdateContentItemStatusHandler(
|
public class UpdateContentItemStatusHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
|
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<UpdateContentItemStatusRequest, ContentItemDetailDto>
|
: Endpoint<UpdateContentItemStatusRequest, ContentItemDetailDto>
|
||||||
{
|
{
|
||||||
private static readonly HashSet<string> AllowedStatuses =
|
private static readonly HashSet<string> AllowedStatuses =
|
||||||
[
|
[
|
||||||
"Draft",
|
"Draft",
|
||||||
"In internal review",
|
"In production",
|
||||||
"Changes requested internally",
|
"In approval",
|
||||||
"Internal changes in progress",
|
|
||||||
"Ready for client review",
|
|
||||||
"In client review",
|
|
||||||
"Changes requested by client",
|
|
||||||
"Client changes in progress",
|
|
||||||
"Approved",
|
"Approved",
|
||||||
"Rejected",
|
"Scheduled",
|
||||||
"Ready to publish",
|
|
||||||
"Published",
|
"Published",
|
||||||
"Archived",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -69,7 +68,64 @@ public class UpdateContentItemStatusHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.Status = normalizedStatus;
|
Workspace? workspace = await dbContext.Workspaces.SingleOrDefaultAsync(candidate => candidate.Id == item.WorkspaceId, ct);
|
||||||
|
if (workspace is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedStatus == "In approval" && workspace.ApprovalMode == ApprovalModes.MultiLevel)
|
||||||
|
{
|
||||||
|
ApprovalWorkflowStartResult startResult = await approvalWorkflowRuntimeService.StartMultiLevelWorkflowAsync(
|
||||||
|
item,
|
||||||
|
workspace,
|
||||||
|
User.GetUserId(),
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (!startResult.Succeeded)
|
||||||
|
{
|
||||||
|
AddError(request => request.Status, startResult.ErrorMessage ?? "The approval workflow could not be started.");
|
||||||
|
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (ApprovalWorkflowRules.IsApprovalCompletionStatus(normalizedStatus) &&
|
||||||
|
ApprovalWorkflowRules.BlocksManualApprovedOrScheduledStatus(workspace.ApprovalMode))
|
||||||
|
{
|
||||||
|
if (workspace.ApprovalMode == ApprovalModes.MultiLevel)
|
||||||
|
{
|
||||||
|
bool hasCompletedWorkflow = await approvalWorkflowRuntimeService.HasCompletedMultiLevelWorkflowAsync(item.Id, ct);
|
||||||
|
|
||||||
|
if (!hasCompletedWorkflow)
|
||||||
|
{
|
||||||
|
AddError(request => request.Status, "This workspace requires the multi-level approval workflow to complete before content can be approved or scheduled.");
|
||||||
|
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bool hasApprovedDecision = await dbContext.ApprovalRequests.AnyAsync(
|
||||||
|
approval => approval.ContentItemId == item.Id &&
|
||||||
|
approval.WorkspaceId == item.WorkspaceId &&
|
||||||
|
approval.State == "Approved" &&
|
||||||
|
approval.CompletedAt.HasValue,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (!hasApprovedDecision)
|
||||||
|
{
|
||||||
|
AddError(request => request.Status, "This workspace requires approval before content can be approved or scheduled.");
|
||||||
|
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.Status != "In approval" || normalizedStatus != "In approval")
|
||||||
|
{
|
||||||
|
item.Status = normalizedStatus;
|
||||||
|
}
|
||||||
await dbContext.SaveChangesAsync(ct);
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
await notificationEventWriter.WriteAsync(
|
await notificationEventWriter.WriteAsync(
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
using Socialize.Api.Modules.Feedback.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Feedback.Contracts;
|
||||||
|
|
||||||
|
public record FeedbackContextDto(
|
||||||
|
Guid? WorkspaceId,
|
||||||
|
string? WorkspaceName,
|
||||||
|
Guid? ClientId,
|
||||||
|
string? ClientName,
|
||||||
|
Guid? ProjectId,
|
||||||
|
string? ProjectName,
|
||||||
|
Guid? ContentItemId,
|
||||||
|
string? ContentItemTitle);
|
||||||
|
|
||||||
|
public record FeedbackMetadataDto(
|
||||||
|
string SubmittedPath,
|
||||||
|
string? BrowserUserAgent,
|
||||||
|
int? ViewportWidth,
|
||||||
|
int? ViewportHeight,
|
||||||
|
string? AppVersion);
|
||||||
|
|
||||||
|
public record FeedbackScreenshotDto(
|
||||||
|
Guid Id,
|
||||||
|
string FileName,
|
||||||
|
string ContentType,
|
||||||
|
long SizeBytes,
|
||||||
|
string DownloadPath,
|
||||||
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
|
public record FeedbackReportDto(
|
||||||
|
Guid Id,
|
||||||
|
string Type,
|
||||||
|
string Status,
|
||||||
|
string Description,
|
||||||
|
Guid ReporterUserId,
|
||||||
|
string ReporterDisplayName,
|
||||||
|
string ReporterEmail,
|
||||||
|
FeedbackMetadataDto Metadata,
|
||||||
|
FeedbackContextDto Context,
|
||||||
|
FeedbackScreenshotDto? Screenshot,
|
||||||
|
IReadOnlyCollection<string> Tags,
|
||||||
|
IReadOnlyCollection<FeedbackTimelineItemDto> Timeline,
|
||||||
|
DateTimeOffset CreatedAt,
|
||||||
|
DateTimeOffset LastActivityAt,
|
||||||
|
DateTimeOffset? CancelledAt,
|
||||||
|
string? CancellationReason);
|
||||||
|
|
||||||
|
public record FeedbackTimelineItemDto(
|
||||||
|
Guid Id,
|
||||||
|
string Kind,
|
||||||
|
Guid ActorUserId,
|
||||||
|
string ActorDisplayName,
|
||||||
|
string ActorEmail,
|
||||||
|
string? ActorRole,
|
||||||
|
string? Body,
|
||||||
|
string? ActivityType,
|
||||||
|
string? FromValue,
|
||||||
|
string? ToValue,
|
||||||
|
string? Note,
|
||||||
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
|
public static class FeedbackDtoMapper
|
||||||
|
{
|
||||||
|
public static FeedbackReportDto ToDto(this FeedbackReport report)
|
||||||
|
{
|
||||||
|
return new FeedbackReportDto(
|
||||||
|
report.Id,
|
||||||
|
ToDisplayString(report.Type),
|
||||||
|
ToDisplayString(report.Status),
|
||||||
|
report.Description,
|
||||||
|
report.ReporterUserId,
|
||||||
|
report.ReporterDisplayName,
|
||||||
|
report.ReporterEmail,
|
||||||
|
new FeedbackMetadataDto(
|
||||||
|
report.SubmittedPath,
|
||||||
|
report.BrowserUserAgent,
|
||||||
|
report.ViewportWidth,
|
||||||
|
report.ViewportHeight,
|
||||||
|
report.AppVersion),
|
||||||
|
new FeedbackContextDto(
|
||||||
|
report.WorkspaceId,
|
||||||
|
report.WorkspaceName,
|
||||||
|
report.ClientId,
|
||||||
|
report.ClientName,
|
||||||
|
report.ProjectId,
|
||||||
|
report.ProjectName,
|
||||||
|
report.ContentItemId,
|
||||||
|
report.ContentItemTitle),
|
||||||
|
report.Screenshot is null
|
||||||
|
? null
|
||||||
|
: new FeedbackScreenshotDto(
|
||||||
|
report.Screenshot.Id,
|
||||||
|
report.Screenshot.FileName,
|
||||||
|
report.Screenshot.ContentType,
|
||||||
|
report.Screenshot.SizeBytes,
|
||||||
|
$"/api/feedback/{report.Id}/screenshot",
|
||||||
|
report.Screenshot.CreatedAt),
|
||||||
|
report.Tags.OrderBy(tag => tag.Name).Select(tag => tag.Name).ToArray(),
|
||||||
|
report.Comments
|
||||||
|
.Select(comment => comment.ToTimelineDto())
|
||||||
|
.Concat(report.ActivityEntries.Select(activity => activity.ToTimelineDto()))
|
||||||
|
.OrderBy(item => item.CreatedAt)
|
||||||
|
.ThenBy(item => item.Kind)
|
||||||
|
.ToArray(),
|
||||||
|
report.CreatedAt,
|
||||||
|
report.LastActivityAt,
|
||||||
|
report.CancelledAt,
|
||||||
|
report.CancellationReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToDisplayString(FeedbackType type)
|
||||||
|
{
|
||||||
|
return type.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToDisplayString(FeedbackStatus status)
|
||||||
|
{
|
||||||
|
return status == FeedbackStatus.WontDo ? "Won't Do" : status.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FeedbackTimelineItemDto ToTimelineDto(this FeedbackComment comment)
|
||||||
|
{
|
||||||
|
return new FeedbackTimelineItemDto(
|
||||||
|
comment.Id,
|
||||||
|
"Comment",
|
||||||
|
comment.AuthorUserId,
|
||||||
|
comment.AuthorDisplayName,
|
||||||
|
comment.AuthorEmail,
|
||||||
|
comment.AuthorRole,
|
||||||
|
comment.Body,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
comment.CreatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FeedbackTimelineItemDto ToTimelineDto(this FeedbackActivityEntry activity)
|
||||||
|
{
|
||||||
|
return new FeedbackTimelineItemDto(
|
||||||
|
activity.Id,
|
||||||
|
"Activity",
|
||||||
|
activity.ActorUserId,
|
||||||
|
activity.ActorDisplayName,
|
||||||
|
activity.ActorEmail,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
activity.ActivityType,
|
||||||
|
activity.FromValue,
|
||||||
|
activity.ToValue,
|
||||||
|
activity.Note,
|
||||||
|
activity.CreatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToFeedbackDisplayString(this FeedbackType type)
|
||||||
|
{
|
||||||
|
return ToDisplayString(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToFeedbackDisplayString(this FeedbackStatus status)
|
||||||
|
{
|
||||||
|
return ToDisplayString(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Socialize.Api.Modules.Feedback.Data;
|
||||||
|
|
||||||
|
public class FeedbackActivityEntry
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid FeedbackReportId { get; set; }
|
||||||
|
public Guid ActorUserId { get; set; }
|
||||||
|
public string ActorDisplayName { get; set; } = string.Empty;
|
||||||
|
public string ActorEmail { get; set; } = string.Empty;
|
||||||
|
public string ActivityType { get; set; } = string.Empty;
|
||||||
|
public string? FromValue { get; set; }
|
||||||
|
public string? ToValue { get; set; }
|
||||||
|
public string? Note { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public FeedbackReport? FeedbackReport { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Socialize.Api.Modules.Feedback.Data;
|
||||||
|
|
||||||
|
public class FeedbackComment
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid FeedbackReportId { get; set; }
|
||||||
|
public Guid AuthorUserId { get; set; }
|
||||||
|
public string AuthorDisplayName { get; set; } = string.Empty;
|
||||||
|
public string AuthorEmail { get; set; } = string.Empty;
|
||||||
|
public string AuthorRole { get; set; } = string.Empty;
|
||||||
|
public string Body { get; set; } = string.Empty;
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public FeedbackReport? FeedbackReport { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Feedback.Data;
|
||||||
|
|
||||||
|
public static class FeedbackModelConfiguration
|
||||||
|
{
|
||||||
|
public static ModelBuilder ConfigureFeedbackModule(this ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<FeedbackReport>(feedback =>
|
||||||
|
{
|
||||||
|
feedback.ToTable("FeedbackReports");
|
||||||
|
feedback.HasKey(x => x.Id);
|
||||||
|
feedback.Property(x => x.Type).HasConversion<string>().HasMaxLength(32).IsRequired();
|
||||||
|
feedback.Property(x => x.Status).HasConversion<string>().HasMaxLength(32).IsRequired();
|
||||||
|
feedback.Property(x => x.Description).HasMaxLength(8000).IsRequired();
|
||||||
|
feedback.Property(x => x.ReporterDisplayName).HasMaxLength(256).IsRequired();
|
||||||
|
feedback.Property(x => x.ReporterEmail).HasMaxLength(256).IsRequired();
|
||||||
|
feedback.Property(x => x.SubmittedPath).HasMaxLength(2048).IsRequired();
|
||||||
|
feedback.Property(x => x.BrowserUserAgent).HasMaxLength(1024);
|
||||||
|
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.ContentItemTitle).HasMaxLength(256);
|
||||||
|
feedback.Property(x => x.CancellationReason).HasMaxLength(2000);
|
||||||
|
feedback.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
feedback.HasIndex(x => x.ReporterUserId);
|
||||||
|
feedback.HasIndex(x => x.Status);
|
||||||
|
feedback.HasIndex(x => x.Type);
|
||||||
|
feedback.HasIndex(x => x.WorkspaceId);
|
||||||
|
feedback.HasIndex(x => x.LastActivityAt);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<FeedbackTag>(tag =>
|
||||||
|
{
|
||||||
|
tag.ToTable("FeedbackTags");
|
||||||
|
tag.HasKey(x => x.Id);
|
||||||
|
tag.Property(x => x.Name).HasMaxLength(64).IsRequired();
|
||||||
|
tag.Property(x => x.NormalizedName).HasMaxLength(64).IsRequired();
|
||||||
|
tag.HasIndex(x => x.NormalizedName);
|
||||||
|
tag.HasIndex(x => new { x.FeedbackReportId, x.NormalizedName }).IsUnique();
|
||||||
|
tag.HasOne(x => x.FeedbackReport)
|
||||||
|
.WithMany(x => x.Tags)
|
||||||
|
.HasForeignKey(x => x.FeedbackReportId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<FeedbackScreenshot>(screenshot =>
|
||||||
|
{
|
||||||
|
screenshot.ToTable("FeedbackScreenshots");
|
||||||
|
screenshot.HasKey(x => x.Id);
|
||||||
|
screenshot.Property(x => x.FileName).HasMaxLength(256).IsRequired();
|
||||||
|
screenshot.Property(x => x.ContentType).HasMaxLength(128).IsRequired();
|
||||||
|
screenshot.Property(x => x.BlobContainerName).HasMaxLength(128).IsRequired();
|
||||||
|
screenshot.Property(x => x.BlobName).HasMaxLength(512).IsRequired();
|
||||||
|
screenshot.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
screenshot.HasIndex(x => x.FeedbackReportId).IsUnique();
|
||||||
|
screenshot.HasOne(x => x.FeedbackReport)
|
||||||
|
.WithOne(x => x.Screenshot)
|
||||||
|
.HasForeignKey<FeedbackScreenshot>(x => x.FeedbackReportId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<FeedbackComment>(comment =>
|
||||||
|
{
|
||||||
|
comment.ToTable("FeedbackComments");
|
||||||
|
comment.HasKey(x => x.Id);
|
||||||
|
comment.Property(x => x.AuthorDisplayName).HasMaxLength(256).IsRequired();
|
||||||
|
comment.Property(x => x.AuthorEmail).HasMaxLength(256).IsRequired();
|
||||||
|
comment.Property(x => x.AuthorRole).HasMaxLength(32).IsRequired();
|
||||||
|
comment.Property(x => x.Body).HasMaxLength(8000).IsRequired();
|
||||||
|
comment.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
comment.HasIndex(x => x.FeedbackReportId);
|
||||||
|
comment.HasIndex(x => x.AuthorUserId);
|
||||||
|
comment.HasIndex(x => x.CreatedAt);
|
||||||
|
comment.HasOne(x => x.FeedbackReport)
|
||||||
|
.WithMany(x => x.Comments)
|
||||||
|
.HasForeignKey(x => x.FeedbackReportId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<FeedbackActivityEntry>(activity =>
|
||||||
|
{
|
||||||
|
activity.ToTable("FeedbackActivityEntries");
|
||||||
|
activity.HasKey(x => x.Id);
|
||||||
|
activity.Property(x => x.ActorDisplayName).HasMaxLength(256).IsRequired();
|
||||||
|
activity.Property(x => x.ActorEmail).HasMaxLength(256).IsRequired();
|
||||||
|
activity.Property(x => x.ActivityType).HasMaxLength(64).IsRequired();
|
||||||
|
activity.Property(x => x.FromValue).HasMaxLength(512);
|
||||||
|
activity.Property(x => x.ToValue).HasMaxLength(512);
|
||||||
|
activity.Property(x => x.Note).HasMaxLength(2000);
|
||||||
|
activity.Property(x => x.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
activity.HasIndex(x => x.FeedbackReportId);
|
||||||
|
activity.HasIndex(x => x.ActorUserId);
|
||||||
|
activity.HasIndex(x => x.CreatedAt);
|
||||||
|
activity.HasOne(x => x.FeedbackReport)
|
||||||
|
.WithMany(x => x.ActivityEntries)
|
||||||
|
.HasForeignKey(x => x.FeedbackReportId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
return modelBuilder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace Socialize.Api.Modules.Feedback.Data;
|
||||||
|
|
||||||
|
public class FeedbackReport
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public FeedbackType Type { get; set; }
|
||||||
|
public FeedbackStatus Status { get; set; }
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public Guid ReporterUserId { get; set; }
|
||||||
|
public string ReporterDisplayName { get; set; } = string.Empty;
|
||||||
|
public string ReporterEmail { get; set; } = string.Empty;
|
||||||
|
public string SubmittedPath { get; set; } = string.Empty;
|
||||||
|
public string? BrowserUserAgent { get; set; }
|
||||||
|
public int? ViewportWidth { get; set; }
|
||||||
|
public int? ViewportHeight { get; set; }
|
||||||
|
public string? AppVersion { get; set; }
|
||||||
|
public Guid? WorkspaceId { get; set; }
|
||||||
|
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? ContentItemId { get; set; }
|
||||||
|
public string? ContentItemTitle { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public DateTimeOffset LastActivityAt { get; set; }
|
||||||
|
public DateTimeOffset? CancelledAt { get; set; }
|
||||||
|
public Guid? CancelledByUserId { get; set; }
|
||||||
|
public string? CancellationReason { get; set; }
|
||||||
|
public ICollection<FeedbackTag> Tags { get; } = new List<FeedbackTag>();
|
||||||
|
public ICollection<FeedbackComment> Comments { get; } = new List<FeedbackComment>();
|
||||||
|
public ICollection<FeedbackActivityEntry> ActivityEntries { get; } = new List<FeedbackActivityEntry>();
|
||||||
|
public FeedbackScreenshot? Screenshot { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Socialize.Api.Modules.Feedback.Data;
|
||||||
|
|
||||||
|
public class FeedbackScreenshot
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid FeedbackReportId { get; set; }
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
public string ContentType { get; set; } = string.Empty;
|
||||||
|
public long SizeBytes { get; set; }
|
||||||
|
public string BlobContainerName { get; set; } = string.Empty;
|
||||||
|
public string BlobName { get; set; } = string.Empty;
|
||||||
|
public DateTimeOffset CreatedAt { get; set; }
|
||||||
|
public FeedbackReport? FeedbackReport { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Socialize.Api.Modules.Feedback.Data;
|
||||||
|
|
||||||
|
public enum FeedbackStatus
|
||||||
|
{
|
||||||
|
New,
|
||||||
|
Planned,
|
||||||
|
Resolved,
|
||||||
|
WontDo,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Socialize.Api.Modules.Feedback.Data;
|
||||||
|
|
||||||
|
public class FeedbackTag
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid FeedbackReportId { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string NormalizedName { get; set; } = string.Empty;
|
||||||
|
public FeedbackReport? FeedbackReport { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Socialize.Api.Modules.Feedback.Data;
|
||||||
|
|
||||||
|
public enum FeedbackType
|
||||||
|
{
|
||||||
|
Bug,
|
||||||
|
Suggestion,
|
||||||
|
Request,
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Socialize.Api.Modules.Feedback;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static WebApplicationBuilder AddFeedbackModule(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Services.AddScoped<Services.FeedbackNotificationService>();
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user