Compare commits
45 Commits
db344eebac
...
feat/googl
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fbb30bb4f | |||
| 2eb54b9228 | |||
| 9c011f1a1e | |||
| b6eb348605 | |||
| 7a8a0a44bf | |||
| 6d92119c9c | |||
| db16e79d9f | |||
| 4aaa1a7f90 | |||
| 6ac05e1a10 | |||
| 9768a37252 | |||
| 98c76a7d88 | |||
| 49e2ca1774 | |||
| e9fb1c5ee0 | |||
| 57abe57bc7 | |||
| 9022fa7d93 | |||
| d1621ecb36 | |||
| 6e417312f9 | |||
| 918136aae2 | |||
| 0521d91240 | |||
| c18a223759 | |||
| 298c46de7c | |||
| 2d22fd6e04 | |||
| ef323c291f | |||
| 4eb0fbc22b | |||
| afe22949c5 | |||
| ebb87b286f | |||
| f1da3a44de | |||
| 419dbf0185 | |||
| 909ae6f092 | |||
| a97ff2dc38 | |||
| 7a862a202a | |||
| 1ae3188d34 | |||
| fb7811c469 | |||
| 0a6d730ca0 | |||
| d2d3bee975 | |||
| 78de068cd1 | |||
| 1965dc2c9e | |||
| f0d635ef21 | |||
| d59d667796 | |||
| 5c0e40db7e | |||
| dc9a980958 | |||
| c40653b2b7 | |||
| f240d32ce6 | |||
| 4775e35b3c | |||
| a7535d460d |
@@ -24,7 +24,7 @@ jobs:
|
|||||||
-t git.mapachotes.com/jbourdon/socialize-api:latest \
|
-t git.mapachotes.com/jbourdon/socialize-api:latest \
|
||||||
-f backend/src/Socialize.Api/Dockerfile .
|
-f backend/src/Socialize.Api/Dockerfile .
|
||||||
docker build \
|
docker build \
|
||||||
--build-arg VITE_API_URL=/api \
|
--build-arg VITE_API_URL=/ \
|
||||||
-t git.mapachotes.com/jbourdon/socialize-web:${{ gitea.sha }} \
|
-t git.mapachotes.com/jbourdon/socialize-web:${{ gitea.sha }} \
|
||||||
-t git.mapachotes.com/jbourdon/socialize-web:latest \
|
-t git.mapachotes.com/jbourdon/socialize-web:latest \
|
||||||
-f frontend/Dockerfile .
|
-f frontend/Dockerfile .
|
||||||
@@ -37,8 +37,9 @@ jobs:
|
|||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
needs: image
|
needs: image
|
||||||
runs-on: bookworm
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
- name: Install SSH client
|
- name: Install SSH client
|
||||||
run: apt-get update && apt-get install -y openssh-client
|
run: apt-get update && apt-get install -y openssh-client
|
||||||
- name: Deploy on sobina
|
- name: Deploy on sobina
|
||||||
@@ -46,9 +47,29 @@ jobs:
|
|||||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||||
DEPLOY_SSH_PRIVATE_KEY_B64: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY_B64 }}
|
DEPLOY_SSH_PRIVATE_KEY_B64: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY_B64 }}
|
||||||
|
SOCIALIZE_IMAGE_TAG: ${{ gitea.sha }}
|
||||||
run: |
|
run: |
|
||||||
|
: "${SOCIALIZE_IMAGE_TAG:?SOCIALIZE_IMAGE_TAG is required}"
|
||||||
|
|
||||||
mkdir -p ~/.ssh
|
mkdir -p ~/.ssh
|
||||||
printf '%s' "$DEPLOY_SSH_PRIVATE_KEY_B64" | base64 -d > ~/.ssh/deploy_key
|
printf '%s' "$DEPLOY_SSH_PRIVATE_KEY_B64" | base64 -d > ~/.ssh/deploy_key
|
||||||
chmod 600 ~/.ssh/deploy_key
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
|
||||||
|
write_env_value() {
|
||||||
|
key="$1"
|
||||||
|
value="$2"
|
||||||
|
escaped_value="$(printf '%s' "$value" | sed "s/'/'\\\\''/g")"
|
||||||
|
printf "%s='%s'\n" "$key" "$escaped_value"
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_env="$(mktemp)"
|
||||||
|
{
|
||||||
|
write_env_value SOCIALIZE_IMAGE_TAG "$SOCIALIZE_IMAGE_TAG"
|
||||||
|
} > "$deploy_env"
|
||||||
|
|
||||||
|
scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new "$deploy_env" "$DEPLOY_USER@$DEPLOY_HOST:/srv/prod/socialize/.deploy.env"
|
||||||
|
rm -f "$deploy_env"
|
||||||
|
scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new deploy/compose.yml "$DEPLOY_USER@$DEPLOY_HOST:/srv/prod/socialize/compose.yml"
|
||||||
|
|
||||||
ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST" \
|
ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||||
'cd /srv/prod/socialize && ./deploy.sh'
|
'test -r /etc/socialize/socialize.env && cd /srv/prod/socialize && ./deploy.sh'
|
||||||
|
|||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -22,6 +22,10 @@ Thumbs.db
|
|||||||
# .NET
|
# .NET
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
|
**/[Bb]in/
|
||||||
|
**/[Oo]bj/
|
||||||
|
**/[Bb]in[\\]*
|
||||||
|
**/[Oo]bj[\\]*
|
||||||
TestResults/
|
TestResults/
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
@@ -30,6 +34,7 @@ dist/
|
|||||||
.vite/
|
.vite/
|
||||||
|
|
||||||
# Local environment files
|
# Local environment files
|
||||||
|
.env
|
||||||
*.local
|
*.local
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
@@ -38,5 +43,11 @@ App_Data/
|
|||||||
# Local SSL certificates
|
# Local SSL certificates
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
# Ai
|
# AI agent local state
|
||||||
|
.agents
|
||||||
|
.agents/
|
||||||
.codex
|
.codex
|
||||||
|
.codex/
|
||||||
|
|
||||||
|
# Generated local artifacts
|
||||||
|
.artifacts/
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -76,6 +76,12 @@ http://localhost:8080
|
|||||||
http://<this-machine-lan-ip>:8080
|
http://<this-machine-lan-ip>:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For preprod deployment, configure the `POSTGRES_PASSWORD`, `RESEND_API_KEY`,
|
||||||
|
`RESEND_FROM_EMAIL`, and `JWT_SIGNING_KEY` Gitea secrets.
|
||||||
|
The deploy workflow writes the remote `.env` file and syncs `deploy/compose.yml`
|
||||||
|
before running the server deploy script.
|
||||||
|
Use the raw Resend API key value for `RESEND_API_KEY`, without a `Bearer ` prefix.
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -90,6 +96,24 @@ cd frontend
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Database Diagram
|
||||||
|
|
||||||
|
Start PostgreSQL, then generate a local schema diagram:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/generate-db-diagram.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script writes an HTML viewer, SVG, PNG, and Graphviz source under:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
.artifacts/db-diagrams/
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `DATABASE_URL`, `PGPASSWORD`, or `~/.pgpass` to provide local database credentials.
|
||||||
|
When using the repository infrastructure script, the diagram script can read from the
|
||||||
|
running `socialize-postgres` container directly.
|
||||||
|
|
||||||
## Agentic Workflow
|
## Agentic Workflow
|
||||||
|
|
||||||
Start here:
|
Start here:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using Microsoft.IdentityModel.Tokens;
|
|||||||
|
|
||||||
namespace Socialize;
|
namespace Socialize;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class ApplicationRegistration
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddWebServices(this IServiceCollection services)
|
public static IServiceCollection AddWebServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
@@ -70,7 +70,6 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
authenticationBuilder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
|
authenticationBuilder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
|
||||||
{
|
{
|
||||||
jwtBearerOptions.Authority = "https://hutopy.com";
|
|
||||||
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
|
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
ValidateIssuer = true,
|
ValidateIssuer = true,
|
||||||
@@ -79,7 +78,7 @@ public static class DependencyInjection
|
|||||||
ValidAudience = authJwt["Audience"],
|
ValidAudience = authJwt["Audience"],
|
||||||
ValidateLifetime = true,
|
ValidateLifetime = true,
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authJwt["Key"] ??
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authJwt["Key"] ??
|
||||||
throw new ArgumentNullException("The Jwt Key is missing.")))
|
throw new InvalidOperationException("Authentication:Jwt:Key is required.")))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -90,9 +89,9 @@ public static class DependencyInjection
|
|||||||
authenticationBuilder.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
|
authenticationBuilder.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
|
||||||
{
|
{
|
||||||
options.ClientId = authGoogle["ClientId"] ??
|
options.ClientId = authGoogle["ClientId"] ??
|
||||||
throw new ArgumentNullException("The Google ClientId is missing.");
|
throw new InvalidOperationException("Authentication:Google:ClientId is required.");
|
||||||
options.ClientSecret = authGoogle["ClientSecret"] ??
|
options.ClientSecret = authGoogle["ClientSecret"] ??
|
||||||
throw new ArgumentNullException("The Google ClientSecret is missing.");
|
throw new InvalidOperationException("Authentication:Google:ClientSecret is required.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +101,9 @@ public static class DependencyInjection
|
|||||||
authenticationBuilder.AddFacebook(FacebookDefaults.AuthenticationScheme, options =>
|
authenticationBuilder.AddFacebook(FacebookDefaults.AuthenticationScheme, options =>
|
||||||
{
|
{
|
||||||
options.ClientId = authFacebook["ClientId"] ??
|
options.ClientId = authFacebook["ClientId"] ??
|
||||||
throw new ArgumentNullException("The Facebook ClientId is missing.");
|
throw new InvalidOperationException("Authentication:Facebook:ClientId is required.");
|
||||||
options.ClientSecret = authFacebook["ClientSecret"] ??
|
options.ClientSecret = authFacebook["ClientSecret"] ??
|
||||||
throw new ArgumentNullException("The Facebook ClientSecret is missing.");
|
throw new InvalidOperationException("Authentication:Facebook:ClientSecret is required.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Common.Domain;
|
namespace Socialize.Api.Common.Domain;
|
||||||
|
|
||||||
public abstract class Entity
|
internal abstract class Entity
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid CreatedBy { get; init; }
|
public Guid CreatedBy { get; init; }
|
||||||
|
|||||||
@@ -12,15 +12,19 @@ using Socialize.Api.Modules.Notifications.Data;
|
|||||||
using Socialize.Api.Modules.Campaigns.Data;
|
using Socialize.Api.Modules.Campaigns.Data;
|
||||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
using Socialize.Api.Modules.Organizations.Data;
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
|
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Data;
|
namespace Socialize.Api.Data;
|
||||||
|
|
||||||
public class AppDbContext(
|
internal class AppDbContext(
|
||||||
DbContextOptions<AppDbContext> options)
|
DbContextOptions<AppDbContext> options)
|
||||||
: IdentityDbContext<User, Role, Guid>(options)
|
: IdentityDbContext<User, Role, Guid>(options)
|
||||||
{
|
{
|
||||||
public DbSet<Organization> Organizations => Set<Organization>();
|
public DbSet<Organization> Organizations => Set<Organization>();
|
||||||
|
public DbSet<OrganizationMembershipTier> OrganizationMembershipTiers => Set<OrganizationMembershipTier>();
|
||||||
|
public DbSet<OrganizationMembershipTierTranslation> OrganizationMembershipTierTranslations =>
|
||||||
|
Set<OrganizationMembershipTierTranslation>();
|
||||||
public DbSet<OrganizationMembership> OrganizationMemberships => Set<OrganizationMembership>();
|
public DbSet<OrganizationMembership> OrganizationMemberships => Set<OrganizationMembership>();
|
||||||
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
||||||
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
||||||
@@ -47,6 +51,10 @@ public class AppDbContext(
|
|||||||
public DbSet<CalendarCatalogEntry> CalendarCatalogEntries => Set<CalendarCatalogEntry>();
|
public DbSet<CalendarCatalogEntry> CalendarCatalogEntries => Set<CalendarCatalogEntry>();
|
||||||
public DbSet<CalendarEvent> CalendarEvents => Set<CalendarEvent>();
|
public DbSet<CalendarEvent> CalendarEvents => Set<CalendarEvent>();
|
||||||
public DbSet<UserCalendarExportFeed> UserCalendarExportFeeds => Set<UserCalendarExportFeed>();
|
public DbSet<UserCalendarExportFeed> UserCalendarExportFeeds => Set<UserCalendarExportFeed>();
|
||||||
|
public DbSet<ReleaseUpdate> ReleaseUpdates => Set<ReleaseUpdate>();
|
||||||
|
public DbSet<ReleaseUpdateReadReceipt> ReleaseUpdateReadReceipts => Set<ReleaseUpdateReadReceipt>();
|
||||||
|
public DbSet<ReleaseCommit> ReleaseCommits => Set<ReleaseCommit>();
|
||||||
|
public DbSet<ReleaseUpdateEmailDigestReceipt> ReleaseUpdateEmailDigestReceipts => Set<ReleaseUpdateEmailDigestReceipt>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
@@ -64,5 +72,6 @@ public class AppDbContext(
|
|||||||
builder.ConfigureNotificationsModule();
|
builder.ConfigureNotificationsModule();
|
||||||
builder.ConfigureFeedbackModule();
|
builder.ConfigureFeedbackModule();
|
||||||
builder.ConfigureCalendarIntegrationsModule();
|
builder.ConfigureCalendarIntegrationsModule();
|
||||||
|
builder.ConfigureReleaseCommunicationsModule();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<wpf:ResourceDictionary xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:s="clr-namespace:System;assembly=mscorlib"
|
|
||||||
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xml:space="preserve">
|
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=hutopy/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
||||||
|
|
||||||
public sealed class LocalBlobStorageOptions
|
internal sealed class LocalBlobStorageOptions
|
||||||
{
|
{
|
||||||
public const string SectionName = "LocalBlobStorage";
|
public const string SectionName = "LocalBlobStorage";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class CommonFileNames
|
internal static class CommonFileNames
|
||||||
{
|
{
|
||||||
public const string ProfilePicture = "profilePicture";
|
public const string ProfilePicture = "profilePicture";
|
||||||
public const string LogoPicture = "logoPicture";
|
public const string LogoPicture = "logoPicture";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class ContentTypes
|
internal static class ContentTypes
|
||||||
{
|
{
|
||||||
private const string ImagePng = "image/png";
|
private const string ImagePng = "image/png";
|
||||||
private const string ImageJpeg = "image/jpeg";
|
private const string ImageJpeg = "image/jpeg";
|
||||||
@@ -39,6 +39,6 @@ public static class ContentTypes
|
|||||||
|
|
||||||
// Check for HTML content by looking for "<!DOCTYPE html>" or "<html>" tags
|
// Check for HTML content by looking for "<!DOCTYPE html>" or "<html>" tags
|
||||||
string content = Encoding.UTF8.GetString(buffer);
|
string content = Encoding.UTF8.GetString(buffer);
|
||||||
return content.Contains("<!DOCTYPE html>");
|
return content.Contains("<!DOCTYPE html>", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public interface IBlobStorage
|
internal interface IBlobStorage
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Upload a file to blob storage.
|
/// Upload a file to blob storage.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class SubDirectoryNames
|
internal static class SubDirectoryNames
|
||||||
{
|
{
|
||||||
public const string Profile = "profile";
|
public const string Profile = "profile";
|
||||||
public const string Contents = "contents";
|
public const string Contents = "contents";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Services;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Services;
|
||||||
|
|
||||||
public sealed class LocalBlobStorage(
|
internal sealed class LocalBlobStorage(
|
||||||
IWebHostEnvironment environment,
|
IWebHostEnvironment environment,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
IOptions<LocalBlobStorageOptions> options,
|
IOptions<LocalBlobStorageOptions> options,
|
||||||
@@ -14,6 +14,14 @@ public sealed class LocalBlobStorage(
|
|||||||
private const long MaxUploadSize = 10 * 1024 * 1024;
|
private const long MaxUploadSize = 10 * 1024 * 1024;
|
||||||
private const string ContentTypeMetadataSuffix = ".content-type";
|
private const string ContentTypeMetadataSuffix = ".content-type";
|
||||||
|
|
||||||
|
private static readonly char[] PathSeparators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, string, string, string, string, Exception?> LogUploadedFile =
|
||||||
|
LoggerMessage.Define<string, string, string, string>(
|
||||||
|
LogLevel.Information,
|
||||||
|
new EventId(1, nameof(UploadFileAsync)),
|
||||||
|
"Blob storage: Uploaded [{BlobName}] to local container [{ContainerName}] with contentType [{ContentType}] and uri [{FileUri}]");
|
||||||
|
|
||||||
private readonly LocalBlobStorageOptions _options = options.Value;
|
private readonly LocalBlobStorageOptions _options = options.Value;
|
||||||
|
|
||||||
public async Task<string> UploadFileAsync(
|
public async Task<string> UploadFileAsync(
|
||||||
@@ -46,12 +54,7 @@ public sealed class LocalBlobStorage(
|
|||||||
await File.WriteAllTextAsync(GetContentTypeMetadataPath(filePath), contentType, ct);
|
await File.WriteAllTextAsync(GetContentTypeMetadataPath(filePath), contentType, ct);
|
||||||
|
|
||||||
string fileUri = BuildPublicUrl(relativePath);
|
string fileUri = BuildPublicUrl(relativePath);
|
||||||
logger.LogInformation(
|
LogUploadedFile(logger, blobName, containerName, contentType, fileUri, null);
|
||||||
"Blob storage: Uploaded [{BlobName}] to local container [{ContainerName}] with contentType [{ContentType}] and uri [{FileUri}]",
|
|
||||||
blobName,
|
|
||||||
containerName,
|
|
||||||
contentType,
|
|
||||||
fileUri);
|
|
||||||
|
|
||||||
return fileUri;
|
return fileUri;
|
||||||
}
|
}
|
||||||
@@ -106,7 +109,7 @@ public sealed class LocalBlobStorage(
|
|||||||
throw new InvalidOperationException("Blob storage: Blob paths must be relative.");
|
throw new InvalidOperationException("Blob storage: Blob paths must be relative.");
|
||||||
}
|
}
|
||||||
|
|
||||||
string[] pathParts = [containerName, .. blobName.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar])];
|
string[] pathParts = [containerName, .. blobName.Split(PathSeparators)];
|
||||||
if (pathParts.Any(part => part is "" or "." or ".."))
|
if (pathParts.Any(part => part is "" or "." or ".."))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Blob storage: Blob paths must not contain relative path segments.");
|
throw new InvalidOperationException("Blob storage: Blob paths must not contain relative path segments.");
|
||||||
@@ -135,7 +138,7 @@ public sealed class LocalBlobStorage(
|
|||||||
? "/api/storage"
|
? "/api/storage"
|
||||||
: requestPath.Trim();
|
: requestPath.Trim();
|
||||||
|
|
||||||
return normalized.StartsWith("/", StringComparison.Ordinal)
|
return normalized.StartsWith('/')
|
||||||
? normalized.TrimEnd('/')
|
? normalized.TrimEnd('/')
|
||||||
: $"/{normalized.TrimEnd('/')}";
|
: $"/{normalized.TrimEnd('/')}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Configuration;
|
namespace Socialize.Api.Infrastructure.Configuration;
|
||||||
|
|
||||||
public class WebsiteOptions
|
internal class WebsiteOptions
|
||||||
{
|
{
|
||||||
public const string SectionName = "Website";
|
public const string SectionName = "Website";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Development;
|
|
||||||
|
|
||||||
public record DevelopmentSeedOptions
|
|
||||||
{
|
|
||||||
public const string SectionName = "DevelopmentSeed";
|
|
||||||
|
|
||||||
public bool Enabled { get; init; } = true;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Emailer.Configuration;
|
namespace Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||||
|
|
||||||
public class EmailerOptions
|
internal class EmailerOptions
|
||||||
{
|
{
|
||||||
public const string ConfigurationSection = "Emailer";
|
public const string ConfigurationSection = "Emailer";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Emailer.Contracts;
|
namespace Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||||
|
|
||||||
public interface IEmailSender
|
internal interface IEmailSender
|
||||||
{
|
{
|
||||||
Task SendEmailAsync(string email, string subject, string message);
|
Task SendEmailAsync(string email, string subject, string message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,19 @@ using Socialize.Api.Infrastructure.Emailer.Contracts;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
public class LoggerEmailSender(ILogger<IEmailSender> logger)
|
internal class LoggerEmailSender(ILogger<IEmailSender> logger)
|
||||||
: IEmailSender
|
: IEmailSender
|
||||||
{
|
{
|
||||||
public async Task SendEmailAsync(string email, string subject, string message)
|
private static readonly Action<ILogger, string, string, string, string, Exception?> LogDevelopmentEmail =
|
||||||
|
LoggerMessage.Define<string, string, string, string>(
|
||||||
|
LogLevel.Information,
|
||||||
|
new EventId(1, nameof(SendEmailAsync)),
|
||||||
|
"Development email to {Email} with subject {Subject}:{NewLine}{Message}");
|
||||||
|
|
||||||
|
public Task SendEmailAsync(string email, string subject, string message)
|
||||||
{
|
{
|
||||||
try
|
LogDevelopmentEmail(logger, email, subject, Environment.NewLine, message, null);
|
||||||
{
|
|
||||||
logger.LogInformation("Sending email to {Email} with subject: {Subject}", email, subject);
|
return Task.CompletedTask;
|
||||||
await Task.Delay(1000);
|
|
||||||
logger.LogInformation("Email sent successfully to {Email}", email);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Failed to send email to {Email}", email);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using PostmarkDotNet;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
public class PostmarkEmailSender : IEmailSender
|
internal class PostmarkEmailSender : IEmailSender
|
||||||
{
|
{
|
||||||
private readonly PostmarkClient _client;
|
private readonly PostmarkClient _client;
|
||||||
private readonly EmailerOptions _options;
|
private readonly EmailerOptions _options;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Options;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
public class ResendEmailSender : IEmailSender
|
internal class ResendEmailSender : IEmailSender
|
||||||
{
|
{
|
||||||
private static readonly Uri EndpointUri = new("https://api.resend.com/emails");
|
private static readonly Uri EndpointUri = new("https://api.resend.com/emails");
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
@@ -20,21 +20,36 @@ public class ResendEmailSender : IEmailSender
|
|||||||
_httpClient = httpClientFactory.CreateClient();
|
_httpClient = httpClientFactory.CreateClient();
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
|
|
||||||
|
string apiKey = NormalizeApiKey(_options.ApiKey);
|
||||||
|
string fromEmail = _options.FromEmail?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(apiKey))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Emailer:ApiKey is required when using Resend email delivery.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(fromEmail))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Emailer:FromEmail is required when using Resend email delivery.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_options.ApiKey = apiKey;
|
||||||
|
_options.FromEmail = fromEmail;
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Authorization =
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
new AuthenticationHeaderValue("Bearer", _options.ApiKey);
|
new AuthenticationHeaderValue("Bearer", apiKey);
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendEmailAsync(string toEmail, string subject, string htmlMessage)
|
public async Task SendEmailAsync(string email, string subject, string message)
|
||||||
{
|
{
|
||||||
var payload = new { from = _options.FromEmail, to = toEmail, subject, html = htmlMessage };
|
var payload = new { from = _options.FromEmail, to = email, subject, html = message };
|
||||||
|
|
||||||
string json = JsonSerializer.Serialize(payload);
|
string json = JsonSerializer.Serialize(payload);
|
||||||
StringContent content = new(json, Encoding.UTF8, "application/json");
|
using StringContent content = new(json, Encoding.UTF8, "application/json");
|
||||||
|
using HttpResponseMessage response = await _httpClient.PostAsync(EndpointUri, content);
|
||||||
HttpResponseMessage response = await _httpClient.PostAsync(EndpointUri, content);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
@@ -43,4 +58,16 @@ public class ResendEmailSender : IEmailSender
|
|||||||
$"Resend email failed: {response.StatusCode} - {body}");
|
$"Resend email failed: {response.StatusCode} - {body}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeApiKey(string? apiKey)
|
||||||
|
{
|
||||||
|
string normalized = apiKey?.Trim().Trim('"', '\'') ?? string.Empty;
|
||||||
|
const string bearerPrefix = "Bearer ";
|
||||||
|
if (normalized.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
normalized = normalized[bearerPrefix.Length..].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using Socialize.Api.Infrastructure.Payments.Stripe.Configuration;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure;
|
namespace Socialize.Api.Infrastructure;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class InfrastructureRegistration
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder AddInfrastructureModule(
|
public static WebApplicationBuilder AddInfrastructureModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
@@ -26,8 +26,14 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
builder.Services.Configure<EmailerOptions>(
|
builder.Services.Configure<EmailerOptions>(
|
||||||
builder.Configuration.GetSection(EmailerOptions.ConfigurationSection));
|
builder.Configuration.GetSection(EmailerOptions.ConfigurationSection));
|
||||||
|
if (builder.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
builder.Services.AddTransient<IEmailSender, LoggerEmailSender>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
builder.Services.AddTransient<IEmailSender, ResendEmailSender>();
|
builder.Services.AddTransient<IEmailSender, ResendEmailSender>();
|
||||||
//builder.Services.AddTransient<IEmailSender, EmailSender>();
|
}
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Payments.Stripe.Configuration;
|
namespace Socialize.Api.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
|
||||||
public class StripeOptions
|
internal class StripeOptions
|
||||||
{
|
{
|
||||||
public const string ConfigurationSection = "Stripe";
|
public const string ConfigurationSection = "Stripe";
|
||||||
|
|
||||||
|
|||||||
@@ -4,52 +4,52 @@ using Socialize.Api.Modules.Organizations.Services;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public sealed class AccessScopeService(
|
internal sealed class AccessScopeService(
|
||||||
OrganizationAccessService organizationAccessService)
|
OrganizationAccessService organizationAccessService)
|
||||||
{
|
{
|
||||||
public bool IsManager(ClaimsPrincipal user)
|
public static bool IsManager(ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
return user.IsInRole(KnownRoles.Administrator) || user.IsInRole(KnownRoles.Manager);
|
return user.IsInRole(KnownRoles.Administrator) || user.IsInRole(KnownRoles.Manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsProvider(ClaimsPrincipal user)
|
public static bool IsProvider(ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
return user.IsInRole(KnownRoles.Provider);
|
return user.IsInRole(KnownRoles.Provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsClient(ClaimsPrincipal user)
|
public static bool IsClient(ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
return user.IsInRole(KnownRoles.Client);
|
return user.IsInRole(KnownRoles.Client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanAccessWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
public static bool CanAccessWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
||||||
{
|
{
|
||||||
return IsManager(user) || user.GetWorkspaceScopeIds().Contains(workspaceId);
|
return IsManager(user) || user.GetWorkspaceScopeIds().Contains(workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanManageWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
public static bool CanManageWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
||||||
{
|
{
|
||||||
return IsManager(user) && CanAccessWorkspace(user, workspaceId);
|
return IsManager(user) && CanAccessWorkspace(user, workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanAccessClient(ClaimsPrincipal user, Guid workspaceId, Guid clientId)
|
public static bool CanAccessClient(ClaimsPrincipal user, Guid workspaceId, Guid clientId)
|
||||||
{
|
{
|
||||||
return IsManager(user)
|
return IsManager(user)
|
||||||
|| (CanAccessWorkspace(user, workspaceId) && user.GetClientScopeIds().Contains(clientId));
|
|| (CanAccessWorkspace(user, workspaceId) && user.GetClientScopeIds().Contains(clientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanAccessCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
public static bool CanAccessCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||||
{
|
{
|
||||||
return IsManager(user)
|
return IsManager(user)
|
||||||
|| (CanAccessClient(user, workspaceId, clientId) && user.GetCampaignScopeIds().Contains(campaignId));
|
|| (CanAccessClient(user, workspaceId, clientId) && user.GetCampaignScopeIds().Contains(campaignId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanContributeToCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
public static bool CanContributeToCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||||
{
|
{
|
||||||
return IsManager(user) || (IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId));
|
return IsManager(user) || (IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
public static bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||||
{
|
{
|
||||||
return IsManager(user)
|
return IsManager(user)
|
||||||
|| IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|
|| IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System.Security.Claims;
|
using System.Globalization;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class ClaimsPrincipalExtensions
|
internal static class ClaimsPrincipalExtensions
|
||||||
{
|
{
|
||||||
public static IReadOnlyCollection<Guid> GetScopeIds(this ClaimsPrincipal claims, string key)
|
public static IReadOnlyCollection<Guid> GetScopeIds(this ClaimsPrincipal claims, string key)
|
||||||
{
|
{
|
||||||
@@ -81,11 +82,11 @@ public static class ClaimsPrincipalExtensions
|
|||||||
|
|
||||||
if (claim is null)
|
if (claim is null)
|
||||||
{
|
{
|
||||||
throw new MissingClaimException(key);
|
throw MissingClaimException.ForClaim(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return typeof(TValue) == typeof(Guid)
|
return typeof(TValue) == typeof(Guid)
|
||||||
? Guid.Parse(claim.Value)
|
? Guid.Parse(claim.Value)
|
||||||
: Convert.ChangeType(claim.Value, typeof(TValue));
|
: Convert.ChangeType(claim.Value, typeof(TValue), CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Microsoft.IdentityModel.Tokens;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class JwtTokenHelper
|
internal static class JwtTokenHelper
|
||||||
{
|
{
|
||||||
public static string GenerateJwtToken(
|
public static string GenerateJwtToken(
|
||||||
TimeSpan expiresIn,
|
TimeSpan expiresIn,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class KnownClaims
|
internal static class KnownClaims
|
||||||
{
|
{
|
||||||
public const string Alias = "alias";
|
public const string Alias = "alias";
|
||||||
public const string PortraitUrl = "portraitUrl";
|
public const string PortraitUrl = "portraitUrl";
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public class MissingClaimException(
|
public class MissingClaimException : Exception
|
||||||
string claimName)
|
{
|
||||||
: Exception($"Claim '{claimName}' is missing.");
|
public MissingClaimException()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MissingClaimException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MissingClaimException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static MissingClaimException ForClaim(string claimName)
|
||||||
|
{
|
||||||
|
return new MissingClaimException($"Claim '{claimName}' is missing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ using System.Text;
|
|||||||
namespace Socialize.Api.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
|
internal static class PasswordGenerator
|
||||||
{
|
{
|
||||||
private const string LowerLetters = "abcdefghijklmnopqrstuvwxyz";
|
private const string LowerLetters = "abcdefghijklmnopqrstuvwxyz";
|
||||||
private const string UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
private const string UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
private const string Numbers = "0123456789";
|
private const string Numbers = "0123456789";
|
||||||
private const string SpecialCharacters = "!@#$%^&*()_+-=[];',./`~{}|:\"<>?";
|
private const string SpecialCharacters = "!@#$%^&*()_+-=[];',./`~{}|:\"<>?";
|
||||||
|
|
||||||
private static readonly Random Random = new();
|
|
||||||
|
|
||||||
public static string Next(
|
public static string Next(
|
||||||
int length = 15,
|
int length = 15,
|
||||||
bool requireNumber = true,
|
bool requireNumber = true,
|
||||||
@@ -23,7 +21,7 @@ public static class PasswordGenerator
|
|||||||
// Create pools based on the requirements
|
// Create pools based on the requirements
|
||||||
StringBuilder characterPool = new();
|
StringBuilder characterPool = new();
|
||||||
|
|
||||||
if (requireNumber)
|
if (requireLowercase)
|
||||||
{
|
{
|
||||||
characterPool.Append(LowerLetters);
|
characterPool.Append(LowerLetters);
|
||||||
}
|
}
|
||||||
@@ -51,22 +49,22 @@ public static class PasswordGenerator
|
|||||||
|
|
||||||
if (requireLowercase)
|
if (requireLowercase)
|
||||||
{
|
{
|
||||||
password[index++] = LowerLetters[Random.Next(LowerLetters.Length)];
|
password[index++] = LowerLetters[RandomNumberGenerator.GetInt32(LowerLetters.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireCapital)
|
if (requireCapital)
|
||||||
{
|
{
|
||||||
password[index++] = UpperLetters[Random.Next(UpperLetters.Length)];
|
password[index++] = UpperLetters[RandomNumberGenerator.GetInt32(UpperLetters.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireNumber)
|
if (requireNumber)
|
||||||
{
|
{
|
||||||
password[index++] = Numbers[Random.Next(Numbers.Length)];
|
password[index++] = Numbers[RandomNumberGenerator.GetInt32(Numbers.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireSpecialCharacter)
|
if (requireSpecialCharacter)
|
||||||
{
|
{
|
||||||
password[index++] = SpecialCharacters[Random.Next(SpecialCharacters.Length)];
|
password[index++] = SpecialCharacters[RandomNumberGenerator.GetInt32(SpecialCharacters.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill the rest with the password
|
// Fill the rest with the password
|
||||||
@@ -85,7 +83,7 @@ public static class PasswordGenerator
|
|||||||
{
|
{
|
||||||
for (int i = array.Length - 1; i > 0; i--)
|
for (int i = array.Length - 1; i > 0; i--)
|
||||||
{
|
{
|
||||||
int j = Random.Next(i + 1);
|
int j = RandomNumberGenerator.GetInt32(i + 1);
|
||||||
(array[i], array[j]) = (array[j], array[i]); // Swap elements
|
(array[i], array[j]) = (array[j], array[i]); // Swap elements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System.Security.Cryptography;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class RefreshTokenGenerator
|
internal static class RefreshTokenGenerator
|
||||||
{
|
{
|
||||||
public static string Next()
|
public static string Next()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ using Socialize.Api.Modules.Campaigns.Data;
|
|||||||
using Socialize.Api.Modules.Organizations.Data;
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
using Socialize.Api.Modules.Organizations.Services;
|
using Socialize.Api.Modules.Organizations.Services;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Services;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Development;
|
namespace Socialize.Api.Infrastructure.TestData;
|
||||||
|
|
||||||
public static class DevelopmentSeedExtensions
|
#pragma warning disable S1075 // Test data intentionally uses representative external URLs.
|
||||||
|
|
||||||
|
internal static class TestDataSeedExtensions
|
||||||
{
|
{
|
||||||
private static readonly Guid OrganizationId = Guid.Parse("99999999-9999-9999-9999-999999999999");
|
private static readonly Guid OrganizationId = Guid.Parse("99999999-9999-9999-9999-999999999999");
|
||||||
private static readonly Guid WorkspaceId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
private static readonly Guid WorkspaceId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||||
@@ -39,23 +41,11 @@ public static class DevelopmentSeedExtensions
|
|||||||
private static readonly Guid ClientCommentId = Guid.Parse("77777777-7777-7777-7777-777777777777");
|
private static readonly Guid ClientCommentId = Guid.Parse("77777777-7777-7777-7777-777777777777");
|
||||||
private static readonly Guid NotificationId = Guid.Parse("88888888-8888-8888-8888-888888888888");
|
private static readonly Guid NotificationId = Guid.Parse("88888888-8888-8888-8888-888888888888");
|
||||||
|
|
||||||
public static async Task<IApplicationBuilder> UseDevelopmentSeedAsync(
|
public static async Task<IServiceProvider> SeedTestDataAsync(
|
||||||
this IApplicationBuilder app,
|
this IServiceProvider services,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
IHostEnvironment environment = app.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
using IServiceScope scope = services.CreateScope();
|
||||||
if (!environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
using IServiceScope scope = app.ApplicationServices.CreateScope();
|
|
||||||
IOptions<DevelopmentSeedOptions> options = scope.ServiceProvider.GetRequiredService<IOptions<DevelopmentSeedOptions>>();
|
|
||||||
if (!options.Value.Enabled)
|
|
||||||
{
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserManager userManager = scope.ServiceProvider.GetRequiredService<UserManager>();
|
UserManager userManager = scope.ServiceProvider.GetRequiredService<UserManager>();
|
||||||
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
|
||||||
@@ -64,7 +54,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
id: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
id: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||||
username: "manager",
|
username: "manager",
|
||||||
email: "manager@socialize.local",
|
email: "manager@socialize.local",
|
||||||
password: "manager",
|
password: "Manager1!",
|
||||||
alias: "Northstar Manager",
|
alias: "Northstar Manager",
|
||||||
firstname: "Morgan",
|
firstname: "Morgan",
|
||||||
lastname: "Reid",
|
lastname: "Reid",
|
||||||
@@ -80,7 +70,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
id: Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
id: Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
||||||
username: "client",
|
username: "client",
|
||||||
email: "client@socialize.local",
|
email: "client@socialize.local",
|
||||||
password: "client",
|
password: "Client1!",
|
||||||
alias: "Sofia Martin",
|
alias: "Sofia Martin",
|
||||||
firstname: "Sofia",
|
firstname: "Sofia",
|
||||||
lastname: "Martin",
|
lastname: "Martin",
|
||||||
@@ -97,7 +87,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
id: Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
|
id: Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
|
||||||
username: "provider",
|
username: "provider",
|
||||||
email: "provider@socialize.local",
|
email: "provider@socialize.local",
|
||||||
password: "provider",
|
password: "Provider1!",
|
||||||
alias: "Alex Studio",
|
alias: "Alex Studio",
|
||||||
firstname: "Alex",
|
firstname: "Alex",
|
||||||
lastname: "Studio",
|
lastname: "Studio",
|
||||||
@@ -115,7 +105,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
id: Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"),
|
id: Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"),
|
||||||
username: "dev",
|
username: "dev",
|
||||||
email: "dev@socialize.local",
|
email: "dev@socialize.local",
|
||||||
password: "dev",
|
password: "Developer1!",
|
||||||
alias: "Socialize Dev",
|
alias: "Socialize Dev",
|
||||||
firstname: "Jo",
|
firstname: "Jo",
|
||||||
lastname: "Bumble",
|
lastname: "Bumble",
|
||||||
@@ -138,7 +128,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
dbContext,
|
dbContext,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return app;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<User> EnsureUserAsync(
|
private static async Task<User> EnsureUserAsync(
|
||||||
@@ -175,7 +165,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
if (!createResult.Succeeded)
|
if (!createResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Failed to seed development user '{username}': {string.Join(", ", createResult.Errors.Select(error => error.Description))}");
|
$"Failed to seed test user '{username}': {string.Join(", ", createResult.Errors.Select(error => error.Description))}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +185,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
if (!passwordResetResult.Succeeded)
|
if (!passwordResetResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Failed to set development password for '{username}': {string.Join(", ", passwordResetResult.Errors.Select(error => error.Description))}");
|
$"Failed to set test password for '{username}': {string.Join(", ", passwordResetResult.Errors.Select(error => error.Description))}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,13 +212,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
await userManager.RemoveClaimAsync(user, claim);
|
await userManager.RemoveClaimAsync(user, claim);
|
||||||
}
|
}
|
||||||
|
|
||||||
string persona = roles.Contains(KnownRoles.Manager, StringComparer.Ordinal)
|
string persona = GetPersona(roles);
|
||||||
? KnownRoles.Manager
|
|
||||||
: roles.Contains(KnownRoles.Client, StringComparer.Ordinal)
|
|
||||||
? KnownRoles.Client
|
|
||||||
: roles.Contains(KnownRoles.Provider, StringComparer.Ordinal)
|
|
||||||
? KnownRoles.Provider
|
|
||||||
: KnownRoles.WorkspaceMember;
|
|
||||||
|
|
||||||
foreach (Claim claim in claims.Concat([new Claim(KnownClaims.Persona, persona)]))
|
foreach (Claim claim in claims.Concat([new Claim(KnownClaims.Persona, persona)]))
|
||||||
{
|
{
|
||||||
@@ -238,6 +222,26 @@ public static class DevelopmentSeedExtensions
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetPersona(IReadOnlyCollection<string> roles)
|
||||||
|
{
|
||||||
|
if (roles.Contains(KnownRoles.Manager, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return KnownRoles.Manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roles.Contains(KnownRoles.Client, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return KnownRoles.Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roles.Contains(KnownRoles.Provider, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return KnownRoles.Provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
return KnownRoles.WorkspaceMember;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task EnsureOrganizationDataAsync(
|
private static async Task EnsureOrganizationDataAsync(
|
||||||
Guid managerUserId,
|
Guid managerUserId,
|
||||||
Guid developerUserId,
|
Guid developerUserId,
|
||||||
@@ -258,6 +262,11 @@ public static class DevelopmentSeedExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
organization.Name = "Northstar Agency";
|
organization.Name = "Northstar Agency";
|
||||||
|
organization.IsGoogleDriveDamEnabled = true;
|
||||||
|
organization.GoogleDriveRootFolderId = "dev-socialize-dam-root";
|
||||||
|
organization.GoogleDriveRootFolderName = "Socialize DAM";
|
||||||
|
organization.GoogleDriveRootFolderUrl = "https://drive.google.com/drive/folders/dev-socialize-dam-root";
|
||||||
|
organization.MembershipTierId = OrganizationMembershipTierSeed.AgencyId;
|
||||||
organization.OwnerUserId = managerUserId;
|
organization.OwnerUserId = managerUserId;
|
||||||
|
|
||||||
await UpsertOrganizationMembershipAsync(
|
await UpsertOrganizationMembershipAsync(
|
||||||
@@ -461,6 +470,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
asset.DisplayName = "Spring launch cut";
|
asset.DisplayName = "Spring launch cut";
|
||||||
asset.GoogleDriveFileId = "dev-socialize-demo";
|
asset.GoogleDriveFileId = "dev-socialize-demo";
|
||||||
asset.GoogleDriveLink = "https://drive.google.com/file/d/dev-socialize-demo/view";
|
asset.GoogleDriveLink = "https://drive.google.com/file/d/dev-socialize-demo/view";
|
||||||
|
asset.GoogleDriveWorkspaceFolderPath = "Socialize DAM/luma-coffee";
|
||||||
asset.PreviewUrl = "https://drive.google.com/thumbnail?id=dev-socialize-demo";
|
asset.PreviewUrl = "https://drive.google.com/thumbnail?id=dev-socialize-demo";
|
||||||
asset.CurrentRevisionNumber = 2;
|
asset.CurrentRevisionNumber = 2;
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
@@ -583,6 +593,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Name = string.Empty,
|
Name = string.Empty,
|
||||||
|
Slug = string.Empty,
|
||||||
TimeZone = string.Empty,
|
TimeZone = string.Empty,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
@@ -590,6 +601,12 @@ public static class DevelopmentSeedExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
workspace.Name = name;
|
workspace.Name = name;
|
||||||
|
workspace.Slug = await WorkspaceSlugGenerator.CreateUniqueAsync(
|
||||||
|
dbContext,
|
||||||
|
organizationId,
|
||||||
|
string.IsNullOrWhiteSpace(workspace.Slug) ? name : workspace.Slug,
|
||||||
|
workspace.Id,
|
||||||
|
cancellationToken);
|
||||||
workspace.OrganizationId = organizationId;
|
workspace.OrganizationId = organizationId;
|
||||||
workspace.OwnerUserId = ownerUserId;
|
workspace.OwnerUserId = ownerUserId;
|
||||||
workspace.TimeZone = timeZone;
|
workspace.TimeZone = timeZone;
|
||||||
@@ -2,7 +2,7 @@ using System.Text.RegularExpressions;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.YouTube;
|
namespace Socialize.Api.Infrastructure.YouTube;
|
||||||
|
|
||||||
public static class YouTubeUrlHelper
|
internal static class YouTubeUrlHelper
|
||||||
{
|
{
|
||||||
private static readonly Regex VideoIdRegex = new(
|
private static readonly Regex VideoIdRegex = new(
|
||||||
@"(?:youtube\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^""&?/\s]{11})",
|
@"(?:youtube\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^""&?/\s]{11})",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using Socialize.Api.Data;
|
|||||||
namespace Socialize.Api.Migrations
|
namespace Socialize.Api.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDbContext))]
|
[DbContext(typeof(AppDbContext))]
|
||||||
[Migration("20260505192305_Initial")]
|
[Migration("20260507143849_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -912,6 +912,29 @@ namespace Socialize.Api.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobContainerName")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobName")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobUrl")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentContentType")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentFileName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<long?>("AttachmentSizeBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
b.Property<string>("AuthorDisplayName")
|
b.Property<string>("AuthorDisplayName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
@@ -5,11 +5,12 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
#pragma warning disable CA1861 // Generated migration seed arrays are not runtime hot paths.
|
||||||
|
|
||||||
namespace Socialize.Api.Migrations
|
namespace Socialize.Api.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class Initial : Migration
|
internal partial class Initial : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
@@ -282,6 +283,12 @@ namespace Socialize.Api.Migrations
|
|||||||
AuthorDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, 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),
|
AuthorEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||||
Body = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
|
Body = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
|
||||||
|
AttachmentFileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
AttachmentContentType = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||||
|
AttachmentSizeBytes = table.Column<long>(type: "bigint", nullable: true),
|
||||||
|
AttachmentBlobContainerName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||||
|
AttachmentBlobName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||||
|
AttachmentBlobUrl = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||||
},
|
},
|
||||||
constraints: table =>
|
constraints: table =>
|
||||||
2173
backend/src/Socialize.Api/Migrations/20260507185052_AddMissingDomainForeignKeys.Designer.cs
generated
Normal file
2173
backend/src/Socialize.Api/Migrations/20260507185052_AddMissingDomainForeignKeys.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,405 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddMissingDomainForeignKeys : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_CampaignId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "CampaignId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_ClientId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "ClientId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_ContentItemId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "ContentItemId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalDecisions_ApprovalRequests_ApprovalRequestId",
|
||||||
|
table: "ApprovalDecisions",
|
||||||
|
column: "ApprovalRequestId",
|
||||||
|
principalTable: "ApprovalRequests",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_ApprovalWorkflowInstances_WorkflowInstance~",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
column: "WorkflowInstanceId",
|
||||||
|
principalTable: "ApprovalWorkflowInstances",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_ContentItems_ContentItemId",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_Workspaces_WorkspaceId",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowInstances_ContentItems_ContentItemId",
|
||||||
|
table: "ApprovalWorkflowInstances",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowInstances_Workspaces_WorkspaceId",
|
||||||
|
table: "ApprovalWorkflowInstances",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_AssetRevisions_Assets_AssetId",
|
||||||
|
table: "AssetRevisions",
|
||||||
|
column: "AssetId",
|
||||||
|
principalTable: "Assets",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Assets_ContentItems_ContentItemId",
|
||||||
|
table: "Assets",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Assets_Workspaces_WorkspaceId",
|
||||||
|
table: "Assets",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Campaigns_Clients_ClientId",
|
||||||
|
table: "Campaigns",
|
||||||
|
column: "ClientId",
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Campaigns_Workspaces_WorkspaceId",
|
||||||
|
table: "Campaigns",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Channels_Workspaces_WorkspaceId",
|
||||||
|
table: "Channels",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Clients_Workspaces_WorkspaceId",
|
||||||
|
table: "Clients",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Comments_Comments_ParentCommentId",
|
||||||
|
table: "Comments",
|
||||||
|
column: "ParentCommentId",
|
||||||
|
principalTable: "Comments",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Comments_ContentItems_ContentItemId",
|
||||||
|
table: "Comments",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Comments_Workspaces_WorkspaceId",
|
||||||
|
table: "Comments",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItemActivityEntries_ContentItems_ContentItemId",
|
||||||
|
table: "ContentItemActivityEntries",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItemActivityEntries_Workspaces_WorkspaceId",
|
||||||
|
table: "ContentItemActivityEntries",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItemRevisions_ContentItems_ContentItemId",
|
||||||
|
table: "ContentItemRevisions",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItems_Campaigns_CampaignId",
|
||||||
|
table: "ContentItems",
|
||||||
|
column: "CampaignId",
|
||||||
|
principalTable: "Campaigns",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItems_Clients_ClientId",
|
||||||
|
table: "ContentItems",
|
||||||
|
column: "ClientId",
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItems_Workspaces_WorkspaceId",
|
||||||
|
table: "ContentItems",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Campaigns_CampaignId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "CampaignId",
|
||||||
|
principalTable: "Campaigns",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Clients_ClientId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "ClientId",
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_FeedbackReports_ContentItems_ContentItemId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Workspaces_WorkspaceId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_NotificationEvents_ContentItems_ContentItemId",
|
||||||
|
table: "NotificationEvents",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_NotificationEvents_Workspaces_WorkspaceId",
|
||||||
|
table: "NotificationEvents",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_WorkspaceApprovalStepConfigurations_Workspaces_WorkspaceId",
|
||||||
|
table: "WorkspaceApprovalStepConfigurations",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_WorkspaceInvites_Workspaces_WorkspaceId",
|
||||||
|
table: "WorkspaceInvites",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalDecisions_ApprovalRequests_ApprovalRequestId",
|
||||||
|
table: "ApprovalDecisions");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_ApprovalWorkflowInstances_WorkflowInstance~",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_ContentItems_ContentItemId",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_Workspaces_WorkspaceId",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowInstances_ContentItems_ContentItemId",
|
||||||
|
table: "ApprovalWorkflowInstances");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowInstances_Workspaces_WorkspaceId",
|
||||||
|
table: "ApprovalWorkflowInstances");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_AssetRevisions_Assets_AssetId",
|
||||||
|
table: "AssetRevisions");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Assets_ContentItems_ContentItemId",
|
||||||
|
table: "Assets");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Assets_Workspaces_WorkspaceId",
|
||||||
|
table: "Assets");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Campaigns_Clients_ClientId",
|
||||||
|
table: "Campaigns");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Campaigns_Workspaces_WorkspaceId",
|
||||||
|
table: "Campaigns");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Channels_Workspaces_WorkspaceId",
|
||||||
|
table: "Channels");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Clients_Workspaces_WorkspaceId",
|
||||||
|
table: "Clients");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Comments_Comments_ParentCommentId",
|
||||||
|
table: "Comments");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Comments_ContentItems_ContentItemId",
|
||||||
|
table: "Comments");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Comments_Workspaces_WorkspaceId",
|
||||||
|
table: "Comments");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItemActivityEntries_ContentItems_ContentItemId",
|
||||||
|
table: "ContentItemActivityEntries");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItemActivityEntries_Workspaces_WorkspaceId",
|
||||||
|
table: "ContentItemActivityEntries");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItemRevisions_ContentItems_ContentItemId",
|
||||||
|
table: "ContentItemRevisions");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItems_Campaigns_CampaignId",
|
||||||
|
table: "ContentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItems_Clients_ClientId",
|
||||||
|
table: "ContentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItems_Workspaces_WorkspaceId",
|
||||||
|
table: "ContentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Campaigns_CampaignId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Clients_ClientId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_FeedbackReports_ContentItems_ContentItemId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Workspaces_WorkspaceId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_NotificationEvents_ContentItems_ContentItemId",
|
||||||
|
table: "NotificationEvents");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_NotificationEvents_Workspaces_WorkspaceId",
|
||||||
|
table: "NotificationEvents");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_WorkspaceApprovalStepConfigurations_Workspaces_WorkspaceId",
|
||||||
|
table: "WorkspaceApprovalStepConfigurations");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_WorkspaceInvites_Workspaces_WorkspaceId",
|
||||||
|
table: "WorkspaceInvites");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_FeedbackReports_CampaignId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_FeedbackReports_ClientId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_FeedbackReports_ContentItemId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2293
backend/src/Socialize.Api/Migrations/20260508001846_AddOrganizationMembershipTiers.Designer.cs
generated
Normal file
2293
backend/src/Socialize.Api/Migrations/20260508001846_AddOrganizationMembershipTiers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddOrganizationMembershipTiers : Migration
|
||||||
|
{
|
||||||
|
private static readonly string[] MembershipTierSeedColumns =
|
||||||
|
[
|
||||||
|
"Id",
|
||||||
|
"ActiveContentLimit",
|
||||||
|
"Description",
|
||||||
|
"ExternalReviewerLimit",
|
||||||
|
"IsCustom",
|
||||||
|
"Key",
|
||||||
|
"MemberLimit",
|
||||||
|
"MonthlyPriceCents",
|
||||||
|
"Name",
|
||||||
|
"SortOrder",
|
||||||
|
"WorkspaceLimit"
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "MembershipTierId",
|
||||||
|
table: "Organizations",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new Guid("20000000-0000-0000-0000-000000000001"));
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationMembershipTiers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||||
|
MonthlyPriceCents = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
WorkspaceLimit = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
ActiveContentLimit = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
MemberLimit = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
ExternalReviewerLimit = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
IsCustom = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
SortOrder = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationMembershipTiers", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
columns: MembershipTierSeedColumns,
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("20000000-0000-0000-0000-000000000001"), 3, "For trying Socialize on one real approval workflow.", 1, false, "free", 2, 0, "Free", 10, 1 },
|
||||||
|
{ new Guid("20000000-0000-0000-0000-000000000002"), 25, "For solo operators managing recurring client reviews.", 10, false, "freelance", 5, 1900, "Freelance", 20, 3 },
|
||||||
|
{ new Guid("20000000-0000-0000-0000-000000000003"), 250, "For agencies that need repeatable client approval operations.", null, false, "agency", 25, 7900, "Agency", 30, 15 },
|
||||||
|
{ new Guid("20000000-0000-0000-0000-000000000004"), null, "For larger organizations with governance and access needs.", null, true, "enterprise", null, null, "Enterprise", 40, null }
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Organizations_MembershipTierId",
|
||||||
|
table: "Organizations",
|
||||||
|
column: "MembershipTierId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationMembershipTiers_Key",
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationMembershipTiers_SortOrder",
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
column: "SortOrder");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Organizations_OrganizationMembershipTiers_MembershipTierId",
|
||||||
|
table: "Organizations",
|
||||||
|
column: "MembershipTierId",
|
||||||
|
principalTable: "OrganizationMembershipTiers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Organizations_OrganizationMembershipTiers_MembershipTierId",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationMembershipTiers");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Organizations_MembershipTierId",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MembershipTierId",
|
||||||
|
table: "Organizations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2382
backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs
generated
Normal file
2382
backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class LocalizeOrganizationMembershipTiers : Migration
|
||||||
|
{
|
||||||
|
private static readonly string[] MembershipTierTranslationSeedColumns =
|
||||||
|
[
|
||||||
|
"Id",
|
||||||
|
"Culture",
|
||||||
|
"Description",
|
||||||
|
"MembershipTierId",
|
||||||
|
"Name"
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly string[] MembershipTierColumnsToRestore =
|
||||||
|
[
|
||||||
|
"Description",
|
||||||
|
"Name"
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Description",
|
||||||
|
table: "OrganizationMembershipTiers");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Name",
|
||||||
|
table: "OrganizationMembershipTiers");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationMembershipTierTranslations",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
MembershipTierId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Culture = table.Column<string>(type: "character varying(16)", maxLength: 16, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationMembershipTierTranslations", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OrganizationMembershipTierTranslations_OrganizationMembersh~",
|
||||||
|
column: x => x.MembershipTierId,
|
||||||
|
principalTable: "OrganizationMembershipTiers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "OrganizationMembershipTierTranslations",
|
||||||
|
columns: MembershipTierTranslationSeedColumns,
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000001"), "en", "For trying Socialize on one real approval workflow.", new Guid("20000000-0000-0000-0000-000000000001"), "Free" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000002"), "fr", "Pour essayer Socialize sur un vrai workflow d'approbation.", new Guid("20000000-0000-0000-0000-000000000001"), "Free" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000003"), "en", "For solo operators managing recurring client reviews.", new Guid("20000000-0000-0000-0000-000000000002"), "Freelance" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000004"), "fr", "Pour les independants qui gerent des revisions client recurrentes.", new Guid("20000000-0000-0000-0000-000000000002"), "Freelance" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000005"), "en", "For agencies that need repeatable client approval operations.", new Guid("20000000-0000-0000-0000-000000000003"), "Agency" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000006"), "fr", "Pour les agences qui veulent des operations d'approbation client repetables.", new Guid("20000000-0000-0000-0000-000000000003"), "Agency" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000007"), "en", "For larger organizations with governance and access needs.", new Guid("20000000-0000-0000-0000-000000000004"), "Enterprise" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000008"), "fr", "Pour les grandes organisations avec des besoins de gouvernance et d'acces.", new Guid("20000000-0000-0000-0000-000000000004"), "Enterprise" }
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationMembershipTierTranslations_MembershipTierId_Cul~",
|
||||||
|
table: "OrganizationMembershipTierTranslations",
|
||||||
|
columns: ["MembershipTierId", "Culture"],
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationMembershipTierTranslations");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Description",
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Name",
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
type: "character varying(128)",
|
||||||
|
maxLength: 128,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000001"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For trying Socialize on one real approval workflow.", "Free" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000002"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For solo operators managing recurring client reviews.", "Freelance" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000003"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For agencies that need repeatable client approval operations.", "Agency" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000004"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For larger organizations with governance and access needs.", "Enterprise" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2628
backend/src/Socialize.Api/Migrations/20260508010206_AddReleaseCommunications.Designer.cs
generated
Normal file
2628
backend/src/Socialize.Api/Migrations/20260508010206_AddReleaseCommunications.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,197 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddReleaseCommunications : Migration
|
||||||
|
{
|
||||||
|
private static readonly string[] ReleaseUpdateReadReceiptUniqueIndexColumns =
|
||||||
|
[
|
||||||
|
"ReleaseUpdateId",
|
||||||
|
"UserId",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||||
|
name: "LastAuthenticatedAt",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ReleaseUpdateEmailDigestReceipts",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
SentAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
UpdateCount = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ReleaseUpdateEmailDigestReceipts", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ReleaseUpdates",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
|
||||||
|
Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||||
|
Body = table.Column<string>(type: "character varying(8000)", maxLength: 8000, nullable: true),
|
||||||
|
Category = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
Importance = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
Audience = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
Status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
DeploymentLabel = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||||
|
BuildVersion = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||||
|
CommitRange = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
CreatedByUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
PublishedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
ArchivedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
ManualEmailSentByUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
ManualEmailSentAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
ManualEmailAudience = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||||
|
ManualEmailRecipientCount = table.Column<int>(type: "integer", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ReleaseUpdates", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ReleaseCommits",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Sha = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
ShortSha = table.Column<string>(type: "character varying(16)", maxLength: 16, nullable: false),
|
||||||
|
Subject = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||||
|
AuthorName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
AuthorEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
AuthoredAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
CommittedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
SourceBranch = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
DeploymentLabel = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||||
|
ExternalUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
CommunicationStatus = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
ReleaseUpdateId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
ImportedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ReleaseCommits", x => x.Sha);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ReleaseCommits_ReleaseUpdates_ReleaseUpdateId",
|
||||||
|
column: x => x.ReleaseUpdateId,
|
||||||
|
principalTable: "ReleaseUpdates",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ReleaseUpdateReadReceipts",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ReleaseUpdateId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ReadAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ReleaseUpdateReadReceipts", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ReleaseUpdateReadReceipts_ReleaseUpdates_ReleaseUpdateId",
|
||||||
|
column: x => x.ReleaseUpdateId,
|
||||||
|
principalTable: "ReleaseUpdates",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseCommits_CommittedAt",
|
||||||
|
table: "ReleaseCommits",
|
||||||
|
column: "CommittedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseCommits_CommunicationStatus",
|
||||||
|
table: "ReleaseCommits",
|
||||||
|
column: "CommunicationStatus");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseCommits_ReleaseUpdateId",
|
||||||
|
table: "ReleaseCommits",
|
||||||
|
column: "ReleaseUpdateId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdateEmailDigestReceipts_SentAt",
|
||||||
|
table: "ReleaseUpdateEmailDigestReceipts",
|
||||||
|
column: "SentAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdateEmailDigestReceipts_UserId",
|
||||||
|
table: "ReleaseUpdateEmailDigestReceipts",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdateReadReceipts_ReleaseUpdateId_UserId",
|
||||||
|
table: "ReleaseUpdateReadReceipts",
|
||||||
|
columns: ReleaseUpdateReadReceiptUniqueIndexColumns,
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdateReadReceipts_UserId",
|
||||||
|
table: "ReleaseUpdateReadReceipts",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdates_Audience",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
column: "Audience");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdates_CreatedByUserId",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
column: "CreatedByUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdates_PublishedAt",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
column: "PublishedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdates_Status",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
column: "Status");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ReleaseCommits");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ReleaseUpdateEmailDigestReceipts");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ReleaseUpdateReadReceipts");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastAuthenticatedAt",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2657
backend/src/Socialize.Api/Migrations/20260508152102_AddGoogleDriveDamFoundation.Designer.cs
generated
Normal file
2657
backend/src/Socialize.Api/Migrations/20260508152102_AddGoogleDriveDamFoundation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddGoogleDriveDamFoundation : Migration
|
||||||
|
{
|
||||||
|
private static readonly string[] WorkspaceOrganizationSlugIndexColumns =
|
||||||
|
[
|
||||||
|
"OrganizationId",
|
||||||
|
"Slug",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Slug",
|
||||||
|
table: "Workspaces",
|
||||||
|
type: "character varying(96)",
|
||||||
|
maxLength: 96,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.Sql(
|
||||||
|
"""
|
||||||
|
WITH normalized AS (
|
||||||
|
SELECT
|
||||||
|
"Id",
|
||||||
|
"OrganizationId",
|
||||||
|
COALESCE(
|
||||||
|
NULLIF(
|
||||||
|
trim(both '-' from lower(regexp_replace(trim("Name"), '[^a-zA-Z0-9]+', '-', 'g'))),
|
||||||
|
''
|
||||||
|
),
|
||||||
|
'workspace'
|
||||||
|
) AS "BaseSlug"
|
||||||
|
FROM "Workspaces"
|
||||||
|
),
|
||||||
|
numbered AS (
|
||||||
|
SELECT
|
||||||
|
"Id",
|
||||||
|
"BaseSlug",
|
||||||
|
row_number() OVER (PARTITION BY "OrganizationId", "BaseSlug" ORDER BY "CreatedAt", "Id") AS "SlugIndex"
|
||||||
|
FROM normalized
|
||||||
|
)
|
||||||
|
UPDATE "Workspaces"
|
||||||
|
SET "Slug" = left(
|
||||||
|
CASE
|
||||||
|
WHEN numbered."SlugIndex" = 1 THEN numbered."BaseSlug"
|
||||||
|
ELSE numbered."BaseSlug" || '-' || numbered."SlugIndex"
|
||||||
|
END,
|
||||||
|
96
|
||||||
|
)
|
||||||
|
FROM numbered
|
||||||
|
WHERE "Workspaces"."Id" = numbered."Id";
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "GoogleDriveRootFolderId",
|
||||||
|
table: "Organizations",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "GoogleDriveRootFolderName",
|
||||||
|
table: "Organizations",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "GoogleDriveRootFolderUrl",
|
||||||
|
table: "Organizations",
|
||||||
|
type: "character varying(2048)",
|
||||||
|
maxLength: 2048,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsGoogleDriveDamEnabled",
|
||||||
|
table: "Organizations",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "GoogleDriveWorkspaceFolderPath",
|
||||||
|
table: "Assets",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Workspaces_OrganizationId_Slug",
|
||||||
|
table: "Workspaces",
|
||||||
|
columns: WorkspaceOrganizationSlugIndexColumns,
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Workspaces_OrganizationId_Slug",
|
||||||
|
table: "Workspaces");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Slug",
|
||||||
|
table: "Workspaces");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GoogleDriveRootFolderId",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GoogleDriveRootFolderName",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GoogleDriveRootFolderUrl",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsGoogleDriveDamEnabled",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "GoogleDriveWorkspaceFolderPath",
|
||||||
|
table: "Assets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -374,6 +374,10 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(2048)");
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleDriveWorkspaceFolderPath")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
b.Property<string>("PreviewUrl")
|
b.Property<string>("PreviewUrl")
|
||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(2048)");
|
.HasColumnType("character varying(2048)");
|
||||||
@@ -909,6 +913,29 @@ namespace Socialize.Api.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobContainerName")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobName")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobUrl")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentContentType")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentFileName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<long?>("AttachmentSizeBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
b.Property<string>("AuthorDisplayName")
|
b.Property<string>("AuthorDisplayName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
@@ -1336,6 +1363,12 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CampaignId");
|
||||||
|
|
||||||
|
b.HasIndex("ClientId");
|
||||||
|
|
||||||
|
b.HasIndex("ContentItemId");
|
||||||
|
|
||||||
b.HasIndex("LastActivityAt");
|
b.HasIndex("LastActivityAt");
|
||||||
|
|
||||||
b.HasIndex("ReporterUserId");
|
b.HasIndex("ReporterUserId");
|
||||||
@@ -1493,6 +1526,9 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(256)");
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LastAuthenticatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Lastname")
|
b.Property<string>("Lastname")
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(256)");
|
.HasColumnType("character varying(256)");
|
||||||
@@ -1626,10 +1662,32 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleDriveRootFolderId")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleDriveRootFolderName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("GoogleDriveRootFolderUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsGoogleDriveDamEnabled")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<string>("LogoUrl")
|
b.Property<string>("LogoUrl")
|
||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(2048)");
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<Guid>("MembershipTierId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid")
|
||||||
|
.HasDefaultValue(new Guid("20000000-0000-0000-0000-000000000001"));
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
@@ -1640,6 +1698,8 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MembershipTierId");
|
||||||
|
|
||||||
b.HasIndex("OwnerUserId");
|
b.HasIndex("OwnerUserId");
|
||||||
|
|
||||||
b.ToTable("Organizations", (string)null);
|
b.ToTable("Organizations", (string)null);
|
||||||
@@ -1679,6 +1739,407 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("OrganizationMemberships", (string)null);
|
b.ToTable("OrganizationMemberships", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int?>("ActiveContentLimit")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("ExternalReviewerLimit")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsCustom")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<int?>("MemberLimit")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("MonthlyPriceCents")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("WorkspaceLimit")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("SortOrder");
|
||||||
|
|
||||||
|
b.ToTable("OrganizationMembershipTiers", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0000-0000-000000000001"),
|
||||||
|
ActiveContentLimit = 3,
|
||||||
|
ExternalReviewerLimit = 1,
|
||||||
|
IsCustom = false,
|
||||||
|
Key = "free",
|
||||||
|
MemberLimit = 2,
|
||||||
|
MonthlyPriceCents = 0,
|
||||||
|
SortOrder = 10,
|
||||||
|
WorkspaceLimit = 1
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0000-0000-000000000002"),
|
||||||
|
ActiveContentLimit = 25,
|
||||||
|
ExternalReviewerLimit = 10,
|
||||||
|
IsCustom = false,
|
||||||
|
Key = "freelance",
|
||||||
|
MemberLimit = 5,
|
||||||
|
MonthlyPriceCents = 1900,
|
||||||
|
SortOrder = 20,
|
||||||
|
WorkspaceLimit = 3
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0000-0000-000000000003"),
|
||||||
|
ActiveContentLimit = 250,
|
||||||
|
IsCustom = false,
|
||||||
|
Key = "agency",
|
||||||
|
MemberLimit = 25,
|
||||||
|
MonthlyPriceCents = 7900,
|
||||||
|
SortOrder = 30,
|
||||||
|
WorkspaceLimit = 15
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0000-0000-000000000004"),
|
||||||
|
IsCustom = true,
|
||||||
|
Key = "enterprise",
|
||||||
|
SortOrder = 40
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTierTranslation", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Culture")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<Guid>("MembershipTierId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("MembershipTierId", "Culture")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("OrganizationMembershipTierTranslations", (string)null);
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000001"),
|
||||||
|
Culture = "en",
|
||||||
|
Description = "For trying Socialize on one real approval workflow.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000001"),
|
||||||
|
Name = "Free"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000002"),
|
||||||
|
Culture = "fr",
|
||||||
|
Description = "Pour essayer Socialize sur un vrai workflow d'approbation.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000001"),
|
||||||
|
Name = "Free"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000003"),
|
||||||
|
Culture = "en",
|
||||||
|
Description = "For solo operators managing recurring client reviews.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000002"),
|
||||||
|
Name = "Freelance"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000004"),
|
||||||
|
Culture = "fr",
|
||||||
|
Description = "Pour les independants qui gerent des revisions client recurrentes.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000002"),
|
||||||
|
Name = "Freelance"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000005"),
|
||||||
|
Culture = "en",
|
||||||
|
Description = "For agencies that need repeatable client approval operations.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000003"),
|
||||||
|
Name = "Agency"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000006"),
|
||||||
|
Culture = "fr",
|
||||||
|
Description = "Pour les agences qui veulent des operations d'approbation client repetables.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000003"),
|
||||||
|
Name = "Agency"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000007"),
|
||||||
|
Culture = "en",
|
||||||
|
Description = "For larger organizations with governance and access needs.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000004"),
|
||||||
|
Name = "Enterprise"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = new Guid("20000000-0000-0001-0000-000000000008"),
|
||||||
|
Culture = "fr",
|
||||||
|
Description = "Pour les grandes organisations avec des besoins de gouvernance et d'acces.",
|
||||||
|
MembershipTierId = new Guid("20000000-0000-0000-0000-000000000004"),
|
||||||
|
Name = "Enterprise"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseCommit", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Sha")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("AuthorName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("AuthoredAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("CommittedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("CommunicationStatus")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("DeploymentLabel")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("ExternalUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ImportedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ReleaseUpdateId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ShortSha")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(16)
|
||||||
|
.HasColumnType("character varying(16)");
|
||||||
|
|
||||||
|
b.Property<string>("SourceBranch")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("Subject")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Sha");
|
||||||
|
|
||||||
|
b.HasIndex("CommittedAt");
|
||||||
|
|
||||||
|
b.HasIndex("CommunicationStatus");
|
||||||
|
|
||||||
|
b.HasIndex("ReleaseUpdateId");
|
||||||
|
|
||||||
|
b.ToTable("ReleaseCommits", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdate", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ArchivedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Audience")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("Body")
|
||||||
|
.HasMaxLength(8000)
|
||||||
|
.HasColumnType("character varying(8000)");
|
||||||
|
|
||||||
|
b.Property<string>("BuildVersion")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Category")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("CommitRange")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("CreatedByUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("DeploymentLabel")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Importance")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("ManualEmailAudience")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<int?>("ManualEmailRecipientCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("ManualEmailSentAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ManualEmailSentByUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("PublishedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Status")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(160)
|
||||||
|
.HasColumnType("character varying(160)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Audience");
|
||||||
|
|
||||||
|
b.HasIndex("CreatedByUserId");
|
||||||
|
|
||||||
|
b.HasIndex("PublishedAt");
|
||||||
|
|
||||||
|
b.HasIndex("Status");
|
||||||
|
|
||||||
|
b.ToTable("ReleaseUpdates", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdateEmailDigestReceipt", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("SentAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<int>("UpdateCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SentAt");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("ReleaseUpdateEmailDigestReceipts", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdateReadReceipt", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("ReadAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("ReleaseUpdateId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("ReleaseUpdateId", "UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ReleaseUpdateReadReceipts", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -1727,6 +2188,11 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasDefaultValue(false);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<string>("Slug")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(96)
|
||||||
|
.HasColumnType("character varying(96)");
|
||||||
|
|
||||||
b.Property<string>("TimeZone")
|
b.Property<string>("TimeZone")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
@@ -1738,6 +2204,9 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasIndex("OwnerUserId");
|
b.HasIndex("OwnerUserId");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId", "Slug")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Workspaces", (string)null);
|
b.ToTable("Workspaces", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1833,6 +2302,83 @@ namespace Socialize.Api.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalDecision", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Approvals.Data.ApprovalRequest", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ApprovalRequestId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalRequest", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ContentItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkflowInstanceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ContentItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.WorkspaceApprovalStepConfiguration", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ContentItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.AssetRevision", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Assets.Data.Asset", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AssetId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarEvent", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarEvent", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarSource", null)
|
b.HasOne("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarSource", null)
|
||||||
@@ -1842,6 +2388,104 @@ namespace Socialize.Api.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Clients.Data.Client", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ClientId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Channels.Data.Channel", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Comments.Data.Comment", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ContentItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Comments.Data.Comment", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ParentCommentId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Campaigns.Data.Campaign", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CampaignId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Clients.Data.Client", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ClientId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemActivityEntry", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ContentItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ContentItemId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||||
@@ -1864,6 +2508,29 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Navigation("FeedbackReport");
|
b.Navigation("FeedbackReport");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Campaigns.Data.Campaign", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CampaignId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Clients.Data.Client", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ClientId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ContentItemId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackScreenshot", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||||
@@ -1886,6 +2553,29 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Navigation("FeedbackReport");
|
b.Navigation("FeedbackReport");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Notifications.Data.NotificationEvent", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.ContentItems.Data.ContentItem", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ContentItemId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.Organization", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MembershipTierId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null)
|
b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null)
|
||||||
@@ -1895,6 +2585,36 @@ namespace Socialize.Api.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTierTranslation", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Organizations.Data.OrganizationMembershipTier", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("MembershipTierId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseCommit", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdate", "ReleaseUpdate")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ReleaseUpdateId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("ReleaseUpdate");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdateReadReceipt", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdate", "ReleaseUpdate")
|
||||||
|
.WithMany("ReadReceipts")
|
||||||
|
.HasForeignKey("ReleaseUpdateId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("ReleaseUpdate");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null)
|
b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null)
|
||||||
@@ -1904,6 +2624,15 @@ namespace Socialize.Api.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.WorkspaceInvite", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Workspaces.Data.Workspace", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("WorkspaceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("ActivityEntries");
|
b.Navigation("ActivityEntries");
|
||||||
@@ -1914,6 +2643,11 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.Navigation("Tags");
|
b.Navigation("Tags");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.ReleaseCommunications.Data.ReleaseUpdate", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ReadReceipts");
|
||||||
|
});
|
||||||
#pragma warning restore 612, 618
|
#pragma warning restore 612, 618
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Approvals.Data;
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public class ApprovalDecision
|
internal class ApprovalDecision
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid ApprovalRequestId { get; set; }
|
public Guid ApprovalRequestId { get; set; }
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Data;
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public static class ApprovalModelConfiguration
|
internal static class ApprovalModelConfiguration
|
||||||
{
|
{
|
||||||
public static ModelBuilder ConfigureApprovalsModule(this ModelBuilder modelBuilder)
|
public static ModelBuilder ConfigureApprovalsModule(this ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -20,6 +22,14 @@ public static class ApprovalModelConfiguration
|
|||||||
workflowInstance.HasIndex(x => new { x.ContentItemId, x.State })
|
workflowInstance.HasIndex(x => new { x.ContentItemId, x.State })
|
||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasFilter("\"State\" = 'Pending'");
|
.HasFilter("\"State\" = 'Pending'");
|
||||||
|
workflowInstance.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
workflowInstance.HasOne<ContentItem>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ContentItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
|
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
|
||||||
@@ -40,6 +50,18 @@ public static class ApprovalModelConfiguration
|
|||||||
approvalRequest.HasIndex(x => x.ContentItemId);
|
approvalRequest.HasIndex(x => x.ContentItemId);
|
||||||
approvalRequest.HasIndex(x => x.WorkflowInstanceId);
|
approvalRequest.HasIndex(x => x.WorkflowInstanceId);
|
||||||
approvalRequest.HasIndex(x => x.ReviewerEmail);
|
approvalRequest.HasIndex(x => x.ReviewerEmail);
|
||||||
|
approvalRequest.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
approvalRequest.HasOne<ContentItem>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ContentItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
approvalRequest.HasOne<ApprovalWorkflowInstance>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkflowInstanceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<ApprovalDecision>(approvalDecision =>
|
modelBuilder.Entity<ApprovalDecision>(approvalDecision =>
|
||||||
@@ -54,6 +76,10 @@ public static class ApprovalModelConfiguration
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
approvalDecision.HasIndex(x => x.ApprovalRequestId);
|
approvalDecision.HasIndex(x => x.ApprovalRequestId);
|
||||||
|
approvalDecision.HasOne<ApprovalRequest>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ApprovalRequestId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<WorkspaceApprovalStepConfiguration>(approvalStep =>
|
modelBuilder.Entity<WorkspaceApprovalStepConfiguration>(approvalStep =>
|
||||||
@@ -69,6 +95,10 @@ public static class ApprovalModelConfiguration
|
|||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
approvalStep.HasIndex(x => x.WorkspaceId);
|
approvalStep.HasIndex(x => x.WorkspaceId);
|
||||||
approvalStep.HasIndex(x => new { x.WorkspaceId, x.SortOrder }).IsUnique();
|
approvalStep.HasIndex(x => new { x.WorkspaceId, x.SortOrder }).IsUnique();
|
||||||
|
approvalStep.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
return modelBuilder;
|
return modelBuilder;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Approvals.Data;
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public class ApprovalRequest
|
internal class ApprovalRequest
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Approvals.Data;
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public class ApprovalWorkflowInstance
|
internal class ApprovalWorkflowInstance
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Approvals.Data;
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public class WorkspaceApprovalStepConfiguration
|
internal class WorkspaceApprovalStepConfiguration
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ using Socialize.Api.Infrastructure.Security;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||||
|
|
||||||
public record GetApprovalsRequest(Guid ContentItemId);
|
internal record GetApprovalsRequest(Guid ContentItemId);
|
||||||
|
|
||||||
public record ApprovalDecisionDto(
|
internal record ApprovalDecisionDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid ApprovalRequestId,
|
Guid ApprovalRequestId,
|
||||||
string Decision,
|
string Decision,
|
||||||
@@ -20,7 +20,7 @@ public record ApprovalDecisionDto(
|
|||||||
string? DecidedByPortraitUrl,
|
string? DecidedByPortraitUrl,
|
||||||
DateTimeOffset CreatedAt);
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
public record ApprovalRequestDto(
|
internal record ApprovalRequestDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ContentItemId,
|
Guid ContentItemId,
|
||||||
@@ -40,7 +40,7 @@ public record ApprovalRequestDto(
|
|||||||
DateTimeOffset? CompletedAt,
|
DateTimeOffset? CompletedAt,
|
||||||
IReadOnlyCollection<ApprovalDecisionDto> Decisions);
|
IReadOnlyCollection<ApprovalDecisionDto> Decisions);
|
||||||
|
|
||||||
public class GetApprovalsHandler(
|
internal class GetApprovalsHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<GetApprovalsRequest, IReadOnlyCollection<ApprovalRequestDto>>
|
: Endpoint<GetApprovalsRequest, IReadOnlyCollection<ApprovalRequestDto>>
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ using Socialize.Api.Modules.Approvals.Data;
|
|||||||
using Socialize.Api.Modules.Approvals.Services;
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||||
|
|
||||||
public record SubmitApprovalDecisionRequest(
|
internal record SubmitApprovalDecisionRequest(
|
||||||
string Decision,
|
string Decision,
|
||||||
string? ReviewerName,
|
string? ReviewerName,
|
||||||
string? ReviewerEmail);
|
string? ReviewerEmail);
|
||||||
|
|
||||||
public class SubmitApprovalDecisionRequestValidator
|
internal class SubmitApprovalDecisionRequestValidator
|
||||||
: Validator<SubmitApprovalDecisionRequest>
|
: Validator<SubmitApprovalDecisionRequest>
|
||||||
{
|
{
|
||||||
public SubmitApprovalDecisionRequestValidator()
|
public SubmitApprovalDecisionRequestValidator()
|
||||||
@@ -31,7 +32,7 @@ public class SubmitApprovalDecisionRequestValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SubmitApprovalDecisionHandler(
|
internal class SubmitApprovalDecisionHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||||
@@ -79,12 +80,14 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
string normalizedDecision = request.Decision.Trim();
|
string normalizedDecision = request.Decision.Trim();
|
||||||
string decidedByName = User?.Identity?.IsAuthenticated == true
|
ClaimsPrincipal? currentUser = User;
|
||||||
? User.GetAlias() ?? User.GetName()
|
bool isAuthenticated = currentUser?.Identity?.IsAuthenticated == true;
|
||||||
: string.IsNullOrWhiteSpace(request.ReviewerName) ? approval.ReviewerName : request.ReviewerName.Trim();
|
string decidedByName = isAuthenticated
|
||||||
string decidedByEmail = User?.Identity?.IsAuthenticated == true
|
? currentUser!.GetAlias() ?? currentUser!.GetName()
|
||||||
? User.GetEmail()
|
: GetReviewerName(request.ReviewerName, approval.ReviewerName);
|
||||||
: string.IsNullOrWhiteSpace(request.ReviewerEmail) ? approval.ReviewerEmail : request.ReviewerEmail.Trim();
|
string decidedByEmail = isAuthenticated
|
||||||
|
? currentUser!.GetEmail()
|
||||||
|
: GetReviewerEmail(request.ReviewerEmail, approval.ReviewerEmail);
|
||||||
|
|
||||||
ApprovalDecision decision = new()
|
ApprovalDecision decision = new()
|
||||||
{
|
{
|
||||||
@@ -207,4 +210,18 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
|
|
||||||
await SendOkAsync(dto, ct);
|
await SendOkAsync(dto, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetReviewerName(string? requestedName, string fallbackName)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(requestedName)
|
||||||
|
? fallbackName
|
||||||
|
: requestedName.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetReviewerEmail(string? requestedEmail, string fallbackEmail)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(requestedEmail)
|
||||||
|
? fallbackEmail
|
||||||
|
: requestedEmail.Trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using Socialize.Api.Modules.Approvals.Services;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals;
|
namespace Socialize.Api.Modules.Approvals;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class ModuleRegistration
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder AddApprovalsModule(
|
public static WebApplicationBuilder AddApprovalsModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
@@ -2,20 +2,20 @@ using Socialize.Api.Modules.Identity.Contracts;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Services;
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
public static class ApprovalStepTargetTypes
|
internal static class ApprovalStepTargetTypes
|
||||||
{
|
{
|
||||||
public const string Role = "Role";
|
public const string Role = "Role";
|
||||||
public const string Membership = "Membership";
|
public const string Membership = "Membership";
|
||||||
public const string Member = "Member";
|
public const string Member = "Member";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ApprovalMembershipTargets
|
internal static class ApprovalMembershipTargets
|
||||||
{
|
{
|
||||||
public const string Team = "Team";
|
public const string Team = "Team";
|
||||||
public const string Client = "Client";
|
public const string Client = "Client";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ApprovalStepConfigurationRules
|
internal static class ApprovalStepConfigurationRules
|
||||||
{
|
{
|
||||||
public static readonly IReadOnlySet<string> AllowedTargetTypes = new HashSet<string>(StringComparer.Ordinal)
|
public static readonly IReadOnlySet<string> AllowedTargetTypes = new HashSet<string>(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using Socialize.Api.Modules.Identity.Contracts;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Services;
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
public static class ApprovalModes
|
internal static class ApprovalModes
|
||||||
{
|
{
|
||||||
public const string None = "None";
|
public const string None = "None";
|
||||||
public const string Optional = "Optional";
|
public const string Optional = "Optional";
|
||||||
@@ -10,7 +10,7 @@ public static class ApprovalModes
|
|||||||
public const string MultiLevel = "Multi-level";
|
public const string MultiLevel = "Multi-level";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ApprovalWorkflowRules
|
internal static class ApprovalWorkflowRules
|
||||||
{
|
{
|
||||||
public static bool BlocksManualApprovedOrScheduledStatus(string approvalMode)
|
public static bool BlocksManualApprovedOrScheduledStatus(string approvalMode)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ using Socialize.Api.Modules.Workspaces.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Services;
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
public record ApprovalWorkflowStartResult(bool Succeeded, string? ErrorMessage);
|
internal record ApprovalWorkflowStartResult(bool Succeeded, string? ErrorMessage);
|
||||||
|
|
||||||
public record ApprovalWorkflowDecisionResult(
|
internal record ApprovalWorkflowDecisionResult(
|
||||||
bool Succeeded,
|
bool Succeeded,
|
||||||
string? ErrorMessage,
|
string? ErrorMessage,
|
||||||
int StatusCode,
|
int StatusCode,
|
||||||
bool IsWorkflowStep);
|
bool IsWorkflowStep);
|
||||||
|
|
||||||
public class ApprovalWorkflowRuntimeService(
|
internal class ApprovalWorkflowRuntimeService(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
{
|
{
|
||||||
@@ -145,13 +145,15 @@ public class ApprovalWorkflowRuntimeService(
|
|||||||
dbContext.ApprovalDecisions.Add(decision);
|
dbContext.ApprovalDecisions.Add(decision);
|
||||||
await dbContext.SaveChangesAsync(ct);
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
int approvedCount = await dbContext.ApprovalDecisions
|
var approvalDecisionParticipants = await dbContext.ApprovalDecisions
|
||||||
.Where(candidate => candidate.ApprovalRequestId == approval.Id && candidate.Decision == ApprovedState)
|
.Where(candidate => candidate.ApprovalRequestId == approval.Id && candidate.Decision == ApprovedState)
|
||||||
.Select(candidate => candidate.DecidedByUserId.HasValue
|
.Select(candidate => candidate.DecidedByUserId.HasValue
|
||||||
? candidate.DecidedByUserId.Value.ToString()
|
? candidate.DecidedByUserId.Value.ToString()
|
||||||
: candidate.DecidedByEmail.ToLower())
|
: candidate.DecidedByEmail)
|
||||||
.Distinct()
|
.ToListAsync(ct);
|
||||||
.CountAsync(ct);
|
int approvedCount = approvalDecisionParticipants
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Count();
|
||||||
|
|
||||||
int requiredApproverCount = approval.WorkflowStepRequiredApproverCount ?? 1;
|
int requiredApproverCount = approval.WorkflowStepRequiredApproverCount ?? 1;
|
||||||
if (!ApprovalWorkflowRules.HasRequiredStepApprovals(approvedCount, requiredApproverCount))
|
if (!ApprovalWorkflowRules.HasRequiredStepApprovals(approvedCount, requiredApproverCount))
|
||||||
@@ -394,7 +396,7 @@ public class ApprovalWorkflowRuntimeService(
|
|||||||
|
|
||||||
private static string CreateAccessToken()
|
private static string CreateAccessToken()
|
||||||
{
|
{
|
||||||
return Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant();
|
return Convert.ToHexString(RandomNumberGenerator.GetBytes(16));
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed record ApprovalNotificationRecipient(Guid UserId, string? Email);
|
private sealed record ApprovalNotificationRecipient(Guid UserId, string? Email);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Assets.Data;
|
namespace Socialize.Api.Modules.Assets.Data;
|
||||||
|
|
||||||
public class Asset
|
internal class Asset
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
@@ -10,6 +10,7 @@ public class Asset
|
|||||||
public required string DisplayName { get; set; }
|
public required string DisplayName { get; set; }
|
||||||
public string? GoogleDriveFileId { get; set; }
|
public string? GoogleDriveFileId { get; set; }
|
||||||
public string? GoogleDriveLink { get; set; }
|
public string? GoogleDriveLink { get; set; }
|
||||||
|
public string? GoogleDriveWorkspaceFolderPath { get; set; }
|
||||||
public string? PreviewUrl { get; set; }
|
public string? PreviewUrl { get; set; }
|
||||||
public int CurrentRevisionNumber { get; set; }
|
public int CurrentRevisionNumber { get; set; }
|
||||||
public DateTimeOffset CreatedAt { get; init; }
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets.Data;
|
namespace Socialize.Api.Modules.Assets.Data;
|
||||||
|
|
||||||
public static class AssetModelConfiguration
|
internal static class AssetModelConfiguration
|
||||||
{
|
{
|
||||||
public static ModelBuilder ConfigureAssetsModule(this ModelBuilder modelBuilder)
|
public static ModelBuilder ConfigureAssetsModule(this ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -15,12 +17,21 @@ public static class AssetModelConfiguration
|
|||||||
asset.Property(x => x.DisplayName).HasMaxLength(256).IsRequired();
|
asset.Property(x => x.DisplayName).HasMaxLength(256).IsRequired();
|
||||||
asset.Property(x => x.GoogleDriveFileId).HasMaxLength(256);
|
asset.Property(x => x.GoogleDriveFileId).HasMaxLength(256);
|
||||||
asset.Property(x => x.GoogleDriveLink).HasMaxLength(2048);
|
asset.Property(x => x.GoogleDriveLink).HasMaxLength(2048);
|
||||||
|
asset.Property(x => x.GoogleDriveWorkspaceFolderPath).HasMaxLength(512);
|
||||||
asset.Property(x => x.PreviewUrl).HasMaxLength(2048);
|
asset.Property(x => x.PreviewUrl).HasMaxLength(2048);
|
||||||
asset.Property(x => x.CreatedAt)
|
asset.Property(x => x.CreatedAt)
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
asset.HasIndex(x => x.WorkspaceId);
|
asset.HasIndex(x => x.WorkspaceId);
|
||||||
asset.HasIndex(x => x.ContentItemId);
|
asset.HasIndex(x => x.ContentItemId);
|
||||||
|
asset.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
asset.HasOne<ContentItem>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ContentItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<AssetRevision>(revision =>
|
modelBuilder.Entity<AssetRevision>(revision =>
|
||||||
@@ -35,6 +46,10 @@ public static class AssetModelConfiguration
|
|||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
revision.HasIndex(x => x.AssetId);
|
revision.HasIndex(x => x.AssetId);
|
||||||
revision.HasIndex(x => new { x.AssetId, x.RevisionNumber }).IsUnique();
|
revision.HasIndex(x => new { x.AssetId, x.RevisionNumber }).IsUnique();
|
||||||
|
revision.HasOne<Asset>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.AssetId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
return modelBuilder;
|
return modelBuilder;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Assets.Data;
|
namespace Socialize.Api.Modules.Assets.Data;
|
||||||
|
|
||||||
public class AssetRevision
|
internal class AssetRevision
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid AssetId { get; set; }
|
public Guid AssetId { get; set; }
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ using System.Text.Json;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
public record CreateAssetRevisionRequest(
|
internal record CreateAssetRevisionRequest(
|
||||||
string SourceReference,
|
string SourceReference,
|
||||||
string? PreviewUrl,
|
string? PreviewUrl,
|
||||||
string? Notes);
|
string? Notes);
|
||||||
|
|
||||||
public class CreateAssetRevisionRequestValidator
|
internal class CreateAssetRevisionRequestValidator
|
||||||
: Validator<CreateAssetRevisionRequest>
|
: Validator<CreateAssetRevisionRequest>
|
||||||
{
|
{
|
||||||
public CreateAssetRevisionRequestValidator()
|
public CreateAssetRevisionRequestValidator()
|
||||||
@@ -26,7 +26,7 @@ public class CreateAssetRevisionRequestValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateAssetRevisionHandler(
|
internal class CreateAssetRevisionHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
IContentItemActivityWriter activityWriter,
|
IContentItemActivityWriter activityWriter,
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ using Socialize.Api.Modules.Assets.Data;
|
|||||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
public record CreateGoogleDriveAssetRequest(
|
internal record CreateGoogleDriveAssetRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ContentItemId,
|
Guid ContentItemId,
|
||||||
string AssetType,
|
string AssetType,
|
||||||
@@ -19,7 +21,7 @@ public record CreateGoogleDriveAssetRequest(
|
|||||||
string GoogleDriveLink,
|
string GoogleDriveLink,
|
||||||
string? PreviewUrl);
|
string? PreviewUrl);
|
||||||
|
|
||||||
public class CreateGoogleDriveAssetRequestValidator
|
internal class CreateGoogleDriveAssetRequestValidator
|
||||||
: Validator<CreateGoogleDriveAssetRequest>
|
: Validator<CreateGoogleDriveAssetRequest>
|
||||||
{
|
{
|
||||||
public CreateGoogleDriveAssetRequestValidator()
|
public CreateGoogleDriveAssetRequestValidator()
|
||||||
@@ -34,7 +36,7 @@ public class CreateGoogleDriveAssetRequestValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateGoogleDriveAssetHandler(
|
internal class CreateGoogleDriveAssetHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
IContentItemActivityWriter activityWriter,
|
IContentItemActivityWriter activityWriter,
|
||||||
@@ -67,6 +69,26 @@ public class CreateGoogleDriveAssetHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Workspace? workspace = await dbContext.Workspaces
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == contentItem.WorkspaceId, ct);
|
||||||
|
if (workspace is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Organization? organization = await dbContext.Organizations
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == workspace.OrganizationId, ct);
|
||||||
|
if (organization is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? workspaceFolderPath = organization.IsGoogleDriveDamEnabled
|
||||||
|
? $"{organization.GoogleDriveRootFolderName}/{workspace.Slug}"
|
||||||
|
: null;
|
||||||
|
|
||||||
Asset asset = new()
|
Asset asset = new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
@@ -77,6 +99,7 @@ public class CreateGoogleDriveAssetHandler(
|
|||||||
DisplayName = request.DisplayName.Trim(),
|
DisplayName = request.DisplayName.Trim(),
|
||||||
GoogleDriveFileId = request.GoogleDriveFileId.Trim(),
|
GoogleDriveFileId = request.GoogleDriveFileId.Trim(),
|
||||||
GoogleDriveLink = request.GoogleDriveLink.Trim(),
|
GoogleDriveLink = request.GoogleDriveLink.Trim(),
|
||||||
|
GoogleDriveWorkspaceFolderPath = workspaceFolderPath,
|
||||||
PreviewUrl = string.IsNullOrWhiteSpace(request.PreviewUrl) ? null : request.PreviewUrl.Trim(),
|
PreviewUrl = string.IsNullOrWhiteSpace(request.PreviewUrl) ? null : request.PreviewUrl.Trim(),
|
||||||
CurrentRevisionNumber = 1,
|
CurrentRevisionNumber = 1,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
@@ -111,6 +134,7 @@ public class CreateGoogleDriveAssetHandler(
|
|||||||
assetType = asset.AssetType,
|
assetType = asset.AssetType,
|
||||||
sourceType = asset.SourceType,
|
sourceType = asset.SourceType,
|
||||||
googleDriveFileId = asset.GoogleDriveFileId,
|
googleDriveFileId = asset.GoogleDriveFileId,
|
||||||
|
googleDriveWorkspaceFolderPath = asset.GoogleDriveWorkspaceFolderPath,
|
||||||
currentRevisionNumber = asset.CurrentRevisionNumber,
|
currentRevisionNumber = asset.CurrentRevisionNumber,
|
||||||
})),
|
})),
|
||||||
ct);
|
ct);
|
||||||
@@ -137,6 +161,7 @@ public class CreateGoogleDriveAssetHandler(
|
|||||||
asset.DisplayName,
|
asset.DisplayName,
|
||||||
asset.GoogleDriveFileId,
|
asset.GoogleDriveFileId,
|
||||||
asset.GoogleDriveLink,
|
asset.GoogleDriveLink,
|
||||||
|
asset.GoogleDriveWorkspaceFolderPath,
|
||||||
asset.PreviewUrl,
|
asset.PreviewUrl,
|
||||||
asset.CurrentRevisionNumber,
|
asset.CurrentRevisionNumber,
|
||||||
asset.CreatedAt,
|
asset.CreatedAt,
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ using Socialize.Api.Infrastructure.Security;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
public record GetAssetsRequest(Guid ContentItemId);
|
internal record GetAssetsRequest(Guid ContentItemId);
|
||||||
|
|
||||||
public record AssetRevisionDto(
|
internal record AssetRevisionDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid AssetId,
|
Guid AssetId,
|
||||||
int RevisionNumber,
|
int RevisionNumber,
|
||||||
@@ -17,7 +17,7 @@ public record AssetRevisionDto(
|
|||||||
Guid? CreatedByUserId,
|
Guid? CreatedByUserId,
|
||||||
DateTimeOffset CreatedAt);
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
public record AssetDto(
|
internal record AssetDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ContentItemId,
|
Guid ContentItemId,
|
||||||
@@ -26,12 +26,13 @@ public record AssetDto(
|
|||||||
string DisplayName,
|
string DisplayName,
|
||||||
string? GoogleDriveFileId,
|
string? GoogleDriveFileId,
|
||||||
string? GoogleDriveLink,
|
string? GoogleDriveLink,
|
||||||
|
string? GoogleDriveWorkspaceFolderPath,
|
||||||
string? PreviewUrl,
|
string? PreviewUrl,
|
||||||
int CurrentRevisionNumber,
|
int CurrentRevisionNumber,
|
||||||
DateTimeOffset CreatedAt,
|
DateTimeOffset CreatedAt,
|
||||||
IReadOnlyCollection<AssetRevisionDto> Revisions);
|
IReadOnlyCollection<AssetRevisionDto> Revisions);
|
||||||
|
|
||||||
public class GetAssetsHandler(
|
internal class GetAssetsHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<GetAssetsRequest, IReadOnlyCollection<AssetDto>>
|
: Endpoint<GetAssetsRequest, IReadOnlyCollection<AssetDto>>
|
||||||
@@ -70,6 +71,7 @@ public class GetAssetsHandler(
|
|||||||
asset.DisplayName,
|
asset.DisplayName,
|
||||||
asset.GoogleDriveFileId,
|
asset.GoogleDriveFileId,
|
||||||
asset.GoogleDriveLink,
|
asset.GoogleDriveLink,
|
||||||
|
asset.GoogleDriveWorkspaceFolderPath,
|
||||||
asset.PreviewUrl,
|
asset.PreviewUrl,
|
||||||
asset.CurrentRevisionNumber,
|
asset.CurrentRevisionNumber,
|
||||||
asset.CreatedAt,
|
asset.CreatedAt,
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
|
internal record WorkspaceDamBackingStoreDto(
|
||||||
|
string Type,
|
||||||
|
bool IsConfigured,
|
||||||
|
string? RootFolderId,
|
||||||
|
string? RootFolderName,
|
||||||
|
string? RootFolderUrl);
|
||||||
|
|
||||||
|
internal record WorkspaceDamFolderDto(
|
||||||
|
string Name,
|
||||||
|
string Path);
|
||||||
|
|
||||||
|
internal record WorkspaceDamDto(
|
||||||
|
Guid WorkspaceId,
|
||||||
|
Guid OrganizationId,
|
||||||
|
string WorkspaceName,
|
||||||
|
string WorkspaceSlug,
|
||||||
|
WorkspaceDamBackingStoreDto BackingStore,
|
||||||
|
WorkspaceDamFolderDto? Folder,
|
||||||
|
IReadOnlyCollection<AssetDto> Assets);
|
||||||
|
|
||||||
|
internal class GetWorkspaceDamHandler(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
AccessScopeService accessScopeService)
|
||||||
|
: EndpointWithoutRequest<WorkspaceDamDto>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Get("/api/workspaces/{workspaceId:guid}/dam");
|
||||||
|
Options(o => o.WithTags("Assets"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
Guid workspaceId = Route<Guid>("workspaceId");
|
||||||
|
|
||||||
|
Workspace? workspace = await dbContext.Workspaces
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == workspaceId, ct);
|
||||||
|
if (workspace is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IReadOnlyCollection<Guid> accessibleWorkspaceIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||||
|
if (!accessibleWorkspaceIds.Contains(workspace.Id))
|
||||||
|
{
|
||||||
|
await SendForbiddenAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Organization? organization = await dbContext.Organizations
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == workspace.OrganizationId, ct);
|
||||||
|
if (organization is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceDamBackingStoreDto backingStore = new(
|
||||||
|
organization.IsGoogleDriveDamEnabled ? "GoogleDrive" : "Unconfigured",
|
||||||
|
organization.IsGoogleDriveDamEnabled,
|
||||||
|
organization.GoogleDriveRootFolderId,
|
||||||
|
organization.GoogleDriveRootFolderName,
|
||||||
|
organization.GoogleDriveRootFolderUrl);
|
||||||
|
|
||||||
|
WorkspaceDamFolderDto? folder = organization.IsGoogleDriveDamEnabled
|
||||||
|
? new WorkspaceDamFolderDto(
|
||||||
|
workspace.Slug,
|
||||||
|
$"{organization.GoogleDriveRootFolderName}/{workspace.Slug}")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
List<AssetDto> assets = await dbContext.Assets
|
||||||
|
.Where(asset => asset.WorkspaceId == workspace.Id)
|
||||||
|
.OrderBy(asset => asset.DisplayName)
|
||||||
|
.Select(asset => new AssetDto(
|
||||||
|
asset.Id,
|
||||||
|
asset.WorkspaceId,
|
||||||
|
asset.ContentItemId,
|
||||||
|
asset.AssetType,
|
||||||
|
asset.SourceType,
|
||||||
|
asset.DisplayName,
|
||||||
|
asset.GoogleDriveFileId,
|
||||||
|
asset.GoogleDriveLink,
|
||||||
|
asset.GoogleDriveWorkspaceFolderPath,
|
||||||
|
asset.PreviewUrl,
|
||||||
|
asset.CurrentRevisionNumber,
|
||||||
|
asset.CreatedAt,
|
||||||
|
dbContext.AssetRevisions
|
||||||
|
.Where(revision => revision.AssetId == asset.Id)
|
||||||
|
.OrderByDescending(revision => revision.RevisionNumber)
|
||||||
|
.Select(revision => new AssetRevisionDto(
|
||||||
|
revision.Id,
|
||||||
|
revision.AssetId,
|
||||||
|
revision.RevisionNumber,
|
||||||
|
revision.SourceReference,
|
||||||
|
revision.PreviewUrl,
|
||||||
|
revision.Notes,
|
||||||
|
revision.CreatedByUserId,
|
||||||
|
revision.CreatedAt))
|
||||||
|
.ToList()))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
await SendOkAsync(
|
||||||
|
new WorkspaceDamDto(
|
||||||
|
workspace.Id,
|
||||||
|
workspace.OrganizationId,
|
||||||
|
workspace.Name,
|
||||||
|
workspace.Slug,
|
||||||
|
backingStore,
|
||||||
|
folder,
|
||||||
|
assets),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ using Socialize.Api.Modules.Assets.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets;
|
namespace Socialize.Api.Modules.Assets;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class ModuleRegistration
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder AddAssetsModule(
|
public static WebApplicationBuilder AddAssetsModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
|
||||||
public class CalendarCatalogEntry
|
internal class CalendarCatalogEntry
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public required string Title { get; set; }
|
public required string Title { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
|
||||||
public static class CalendarCatalogSeed
|
#pragma warning disable S1075 // Catalog seed entries intentionally store source URLs.
|
||||||
|
|
||||||
|
internal static class CalendarCatalogSeed
|
||||||
{
|
{
|
||||||
public static readonly CalendarCatalogEntry[] Entries =
|
public static readonly CalendarCatalogEntry[] Entries =
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
|
||||||
public class CalendarEvent
|
internal class CalendarEvent
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid CalendarSourceId { get; set; }
|
public Guid CalendarSourceId { get; set; }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
|
||||||
public class CalendarSource
|
internal class CalendarSource
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public required string Scope { get; set; }
|
public required string Scope { get; set; }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
|
||||||
public static class CalendarSourceModelConfiguration
|
internal static class CalendarSourceModelConfiguration
|
||||||
{
|
{
|
||||||
public static ModelBuilder ConfigureCalendarIntegrationsModule(this ModelBuilder modelBuilder)
|
public static ModelBuilder ConfigureCalendarIntegrationsModule(this ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
|
||||||
public class UserCalendarExportFeed
|
internal class UserCalendarExportFeed
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using Socialize.Api.Modules.CalendarIntegrations.Services;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||||
|
|
||||||
public record CalendarSourceDto(
|
internal record CalendarSourceDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
string Scope,
|
string Scope,
|
||||||
Guid? OrganizationId,
|
Guid? OrganizationId,
|
||||||
@@ -47,7 +47,7 @@ public record CalendarSourceDto(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record UpsertCalendarSourceRequest(
|
internal record UpsertCalendarSourceRequest(
|
||||||
string Scope,
|
string Scope,
|
||||||
Guid? OrganizationId,
|
Guid? OrganizationId,
|
||||||
Guid? WorkspaceId,
|
Guid? WorkspaceId,
|
||||||
@@ -59,7 +59,7 @@ public record UpsertCalendarSourceRequest(
|
|||||||
bool IsEnabled,
|
bool IsEnabled,
|
||||||
string? InheritanceMode);
|
string? InheritanceMode);
|
||||||
|
|
||||||
public class UpsertCalendarSourceRequestValidator
|
internal class UpsertCalendarSourceRequestValidator
|
||||||
: FastEndpoints.Validator<UpsertCalendarSourceRequest>
|
: FastEndpoints.Validator<UpsertCalendarSourceRequest>
|
||||||
{
|
{
|
||||||
public UpsertCalendarSourceRequestValidator()
|
public UpsertCalendarSourceRequestValidator()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Socialize.Api.Modules.Organizations.Services;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||||
|
|
||||||
public class CreateCalendarSourceHandler(
|
internal class CreateCalendarSourceHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
OrganizationAccessService organizationAccessService)
|
OrganizationAccessService organizationAccessService)
|
||||||
@@ -121,7 +121,7 @@ public class CreateCalendarSourceHandler(
|
|||||||
source.CatalogSourceReference == normalizedCatalogReference) ||
|
source.CatalogSourceReference == normalizedCatalogReference) ||
|
||||||
(!string.IsNullOrWhiteSpace(normalizedUrl) &&
|
(!string.IsNullOrWhiteSpace(normalizedUrl) &&
|
||||||
source.SourceUrl != null &&
|
source.SourceUrl != null &&
|
||||||
source.SourceUrl.ToUpper() == normalizedUrl.ToUpper()),
|
EF.Functions.ILike(source.SourceUrl, normalizedUrl)),
|
||||||
ct);
|
ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Socialize.Api.Modules.Organizations.Services;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||||
|
|
||||||
public class DeleteCalendarSourceHandler(
|
internal class DeleteCalendarSourceHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
OrganizationAccessService organizationAccessService)
|
OrganizationAccessService organizationAccessService)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Socialize.Api.Modules.CalendarIntegrations.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||||
|
|
||||||
public sealed class ListCalendarCatalogRequest
|
internal sealed class ListCalendarCatalogRequest
|
||||||
{
|
{
|
||||||
public string? Search { get; set; }
|
public string? Search { get; set; }
|
||||||
public string? Country { get; set; }
|
public string? Country { get; set; }
|
||||||
@@ -16,7 +16,7 @@ public sealed class ListCalendarCatalogRequest
|
|||||||
public string? Provider { get; set; }
|
public string? Provider { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public record CalendarCatalogEntryDto(
|
internal record CalendarCatalogEntryDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
string Title,
|
string Title,
|
||||||
string Description,
|
string Description,
|
||||||
@@ -30,7 +30,7 @@ public record CalendarCatalogEntryDto(
|
|||||||
string TrustLevel,
|
string TrustLevel,
|
||||||
string DefaultColor);
|
string DefaultColor);
|
||||||
|
|
||||||
public class ListCalendarCatalogHandler(AppDbContext dbContext)
|
internal class ListCalendarCatalogHandler(AppDbContext dbContext)
|
||||||
: Endpoint<ListCalendarCatalogRequest, IReadOnlyCollection<CalendarCatalogEntryDto>>
|
: Endpoint<ListCalendarCatalogRequest, IReadOnlyCollection<CalendarCatalogEntryDto>>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -47,11 +47,11 @@ public class ListCalendarCatalogHandler(AppDbContext dbContext)
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.Search))
|
if (!string.IsNullOrWhiteSpace(request.Search))
|
||||||
{
|
{
|
||||||
string search = request.Search.Trim().ToLowerInvariant();
|
string search = $"%{request.Search.Trim()}%";
|
||||||
query = query.Where(entry =>
|
query = query.Where(entry =>
|
||||||
entry.Title.ToLower().Contains(search) ||
|
EF.Functions.ILike(entry.Title, search) ||
|
||||||
entry.Description.ToLower().Contains(search) ||
|
EF.Functions.ILike(entry.Description, search) ||
|
||||||
entry.ProviderName.ToLower().Contains(search));
|
EF.Functions.ILike(entry.ProviderName, search));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.Country))
|
if (!string.IsNullOrWhiteSpace(request.Country))
|
||||||
|
|||||||
@@ -7,14 +7,14 @@ using Socialize.Api.Modules.CalendarIntegrations.Services;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||||
|
|
||||||
public sealed class ListCalendarEventsRequest
|
internal sealed class ListCalendarEventsRequest
|
||||||
{
|
{
|
||||||
public Guid? WorkspaceId { get; set; }
|
public Guid? WorkspaceId { get; set; }
|
||||||
public DateOnly? StartDate { get; set; }
|
public DateOnly? StartDate { get; set; }
|
||||||
public DateOnly? EndDate { get; set; }
|
public DateOnly? EndDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public record CalendarEventDto(
|
internal record CalendarEventDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid CalendarSourceId,
|
Guid CalendarSourceId,
|
||||||
string SourceEventUid,
|
string SourceEventUid,
|
||||||
@@ -35,7 +35,7 @@ public record CalendarEventDto(
|
|||||||
DateTimeOffset? SourceLastModifiedAt,
|
DateTimeOffset? SourceLastModifiedAt,
|
||||||
DateTimeOffset ImportedAt);
|
DateTimeOffset ImportedAt);
|
||||||
|
|
||||||
public class ListCalendarEventsHandler(
|
internal class ListCalendarEventsHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<ListCalendarEventsRequest, IReadOnlyCollection<CalendarEventDto>>
|
: Endpoint<ListCalendarEventsRequest, IReadOnlyCollection<CalendarEventDto>>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ using Socialize.Api.Modules.CalendarIntegrations.Services;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||||
|
|
||||||
public record ListCalendarSourcesRequest(Guid? WorkspaceId);
|
internal record ListCalendarSourcesRequest(Guid? WorkspaceId);
|
||||||
|
|
||||||
public class ListCalendarSourcesHandler(
|
internal class ListCalendarSourcesHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<ListCalendarSourcesRequest, IReadOnlyCollection<CalendarSourceDto>>
|
: Endpoint<ListCalendarSourcesRequest, IReadOnlyCollection<CalendarSourceDto>>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Socialize.Api.Modules.Organizations.Services;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||||
|
|
||||||
public class RefreshCalendarSourceHandler(
|
internal class RefreshCalendarSourceHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
OrganizationAccessService organizationAccessService,
|
OrganizationAccessService organizationAccessService,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ using Socialize.Api.Modules.Organizations.Services;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||||
|
|
||||||
public class UpdateCalendarSourceHandler(
|
internal class UpdateCalendarSourceHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
OrganizationAccessService organizationAccessService)
|
OrganizationAccessService organizationAccessService)
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ using Socialize.Api.Modules.Identity.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||||
|
|
||||||
public record UserCalendarExportFeedDto(
|
internal record UserCalendarExportFeedDto(
|
||||||
bool IsEnabled,
|
bool IsEnabled,
|
||||||
string? FeedUrl,
|
string? FeedUrl,
|
||||||
DateTimeOffset? CreatedAt,
|
DateTimeOffset? CreatedAt,
|
||||||
DateTimeOffset? UpdatedAt,
|
DateTimeOffset? UpdatedAt,
|
||||||
DateTimeOffset? RevokedAt);
|
DateTimeOffset? RevokedAt);
|
||||||
|
|
||||||
public class GetUserCalendarExportFeedHandler(AppDbContext dbContext)
|
internal class GetUserCalendarExportFeedHandler(AppDbContext dbContext)
|
||||||
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -33,7 +33,7 @@ public class GetUserCalendarExportFeedHandler(AppDbContext dbContext)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class EnableUserCalendarExportFeedHandler(AppDbContext dbContext)
|
internal class EnableUserCalendarExportFeedHandler(AppDbContext dbContext)
|
||||||
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -81,7 +81,7 @@ public class EnableUserCalendarExportFeedHandler(AppDbContext dbContext)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RegenerateUserCalendarExportFeedHandler(AppDbContext dbContext)
|
internal class RegenerateUserCalendarExportFeedHandler(AppDbContext dbContext)
|
||||||
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -120,7 +120,7 @@ public class RegenerateUserCalendarExportFeedHandler(AppDbContext dbContext)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RevokeUserCalendarExportFeedHandler(AppDbContext dbContext)
|
internal class RevokeUserCalendarExportFeedHandler(AppDbContext dbContext)
|
||||||
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -147,7 +147,7 @@ public class RevokeUserCalendarExportFeedHandler(AppDbContext dbContext)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GetUserCalendarExportFeedIcsHandler(
|
internal class GetUserCalendarExportFeedIcsHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
CalendarExportFeedService feedService)
|
CalendarExportFeedService feedService)
|
||||||
: EndpointWithoutRequest
|
: EndpointWithoutRequest
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
namespace Socialize.Api.Modules.CalendarIntegrations;
|
namespace Socialize.Api.Modules.CalendarIntegrations;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class ModuleRegistration
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder AddCalendarIntegrationsModule(this WebApplicationBuilder builder)
|
public static WebApplicationBuilder AddCalendarIntegrationsModule(this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
builder.Services.AddSingleton<Services.IcsCalendarParser>();
|
|
||||||
builder.Services.AddSingleton<Services.CalendarExportFeedBuilder>();
|
|
||||||
builder.Services.AddScoped<Services.CalendarExportFeedService>();
|
builder.Services.AddScoped<Services.CalendarExportFeedService>();
|
||||||
builder.Services.AddScoped<Services.CalendarImportSyncService>();
|
builder.Services.AddScoped<Services.CalendarImportSyncService>();
|
||||||
builder.Services.AddHostedService<Services.CalendarImportBackgroundService>();
|
builder.Services.AddHostedService<Services.CalendarImportBackgroundService>();
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||||
|
|
||||||
public sealed record CalendarExportFeedEvent(
|
internal sealed record CalendarExportFeedEvent(
|
||||||
string Uid,
|
string Uid,
|
||||||
string Title,
|
string Title,
|
||||||
DateTimeOffset StartsAt,
|
DateTimeOffset StartsAt,
|
||||||
@@ -11,9 +12,9 @@ public sealed record CalendarExportFeedEvent(
|
|||||||
string? Description,
|
string? Description,
|
||||||
string? Url);
|
string? Url);
|
||||||
|
|
||||||
public class CalendarExportFeedBuilder
|
internal static class CalendarExportFeedBuilder
|
||||||
{
|
{
|
||||||
public string Build(string calendarName, IReadOnlyCollection<CalendarExportFeedEvent> events)
|
public static string Build(string calendarName, IReadOnlyCollection<CalendarExportFeedEvent> events)
|
||||||
{
|
{
|
||||||
StringBuilder builder = new();
|
StringBuilder builder = new();
|
||||||
builder.AppendLine("BEGIN:VCALENDAR");
|
builder.AppendLine("BEGIN:VCALENDAR");
|
||||||
@@ -21,34 +22,34 @@ public class CalendarExportFeedBuilder
|
|||||||
builder.AppendLine("PRODID:-//Socialize//User Work Calendar//EN");
|
builder.AppendLine("PRODID:-//Socialize//User Work Calendar//EN");
|
||||||
builder.AppendLine("CALSCALE:GREGORIAN");
|
builder.AppendLine("CALSCALE:GREGORIAN");
|
||||||
builder.AppendLine("METHOD:PUBLISH");
|
builder.AppendLine("METHOD:PUBLISH");
|
||||||
builder.AppendLine($"X-WR-CALNAME:{EscapeText(calendarName)}");
|
AppendLineInvariant(builder, $"X-WR-CALNAME:{EscapeText(calendarName)}");
|
||||||
|
|
||||||
foreach (CalendarExportFeedEvent feedEvent in events.OrderBy(calendarEvent => calendarEvent.StartsAt))
|
foreach (CalendarExportFeedEvent feedEvent in events.OrderBy(calendarEvent => calendarEvent.StartsAt))
|
||||||
{
|
{
|
||||||
builder.AppendLine("BEGIN:VEVENT");
|
builder.AppendLine("BEGIN:VEVENT");
|
||||||
builder.AppendLine($"UID:{EscapeText(feedEvent.Uid)}");
|
AppendLineInvariant(builder, $"UID:{EscapeText(feedEvent.Uid)}");
|
||||||
builder.AppendLine($"DTSTAMP:{FormatUtc(DateTimeOffset.UtcNow)}");
|
AppendLineInvariant(builder, $"DTSTAMP:{FormatUtc(DateTimeOffset.UtcNow)}");
|
||||||
builder.AppendLine($"SUMMARY:{EscapeText(feedEvent.Title)}");
|
AppendLineInvariant(builder, $"SUMMARY:{EscapeText(feedEvent.Title)}");
|
||||||
|
|
||||||
if (feedEvent.IsAllDay)
|
if (feedEvent.IsAllDay)
|
||||||
{
|
{
|
||||||
builder.AppendLine($"DTSTART;VALUE=DATE:{FormatDate(feedEvent.StartsAt)}");
|
AppendLineInvariant(builder, $"DTSTART;VALUE=DATE:{FormatDate(feedEvent.StartsAt)}");
|
||||||
builder.AppendLine($"DTEND;VALUE=DATE:{FormatDate(feedEvent.EndsAt)}");
|
AppendLineInvariant(builder, $"DTEND;VALUE=DATE:{FormatDate(feedEvent.EndsAt)}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
builder.AppendLine($"DTSTART:{FormatUtc(feedEvent.StartsAt)}");
|
AppendLineInvariant(builder, $"DTSTART:{FormatUtc(feedEvent.StartsAt)}");
|
||||||
builder.AppendLine($"DTEND:{FormatUtc(feedEvent.EndsAt)}");
|
AppendLineInvariant(builder, $"DTEND:{FormatUtc(feedEvent.EndsAt)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(feedEvent.Description))
|
if (!string.IsNullOrWhiteSpace(feedEvent.Description))
|
||||||
{
|
{
|
||||||
builder.AppendLine($"DESCRIPTION:{EscapeText(feedEvent.Description)}");
|
AppendLineInvariant(builder, $"DESCRIPTION:{EscapeText(feedEvent.Description)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(feedEvent.Url))
|
if (!string.IsNullOrWhiteSpace(feedEvent.Url))
|
||||||
{
|
{
|
||||||
builder.AppendLine($"URL:{EscapeText(feedEvent.Url)}");
|
AppendLineInvariant(builder, $"URL:{EscapeText(feedEvent.Url)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.AppendLine("END:VEVENT");
|
builder.AppendLine("END:VEVENT");
|
||||||
@@ -71,10 +72,15 @@ public class CalendarExportFeedBuilder
|
|||||||
private static string EscapeText(string value)
|
private static string EscapeText(string value)
|
||||||
{
|
{
|
||||||
return value
|
return value
|
||||||
.Replace("\\", "\\\\")
|
.Replace("\\", "\\\\", StringComparison.Ordinal)
|
||||||
.Replace("\r\n", "\\n")
|
.Replace("\r\n", "\\n", StringComparison.Ordinal)
|
||||||
.Replace("\n", "\\n")
|
.Replace("\n", "\\n", StringComparison.Ordinal)
|
||||||
.Replace(";", "\\;")
|
.Replace(";", "\\;", StringComparison.Ordinal)
|
||||||
.Replace(",", "\\,");
|
.Replace(",", "\\,", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendLineInvariant(StringBuilder builder, FormattableString value)
|
||||||
|
{
|
||||||
|
builder.AppendLine(value.ToString(CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ using Socialize.Api.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||||
|
|
||||||
public class CalendarExportFeedService(AppDbContext dbContext, CalendarExportFeedBuilder feedBuilder)
|
internal class CalendarExportFeedService(AppDbContext dbContext)
|
||||||
{
|
{
|
||||||
public async Task<string> BuildUserFeedAsync(Guid userId, string? userEmail, string appBaseUrl, CancellationToken ct)
|
public async Task<string> BuildUserFeedAsync(Guid userId, string? userEmail, string appBaseUrl, CancellationToken ct)
|
||||||
{
|
{
|
||||||
string normalizedEmail = userEmail?.Trim().ToUpperInvariant() ?? string.Empty;
|
string normalizedEmail = userEmail?.Trim() ?? string.Empty;
|
||||||
Guid[] workspaceIds = await dbContext.Workspaces
|
Guid[] workspaceIds = await dbContext.Workspaces
|
||||||
.Where(workspace =>
|
.Where(workspace =>
|
||||||
workspace.OwnerUserId == userId ||
|
workspace.OwnerUserId == userId ||
|
||||||
@@ -51,7 +51,7 @@ public class CalendarExportFeedService(AppDbContext dbContext, CalendarExportFee
|
|||||||
.Where(approval =>
|
.Where(approval =>
|
||||||
approval.DueAt.HasValue &&
|
approval.DueAt.HasValue &&
|
||||||
(approval.RequestedByUserId == userId ||
|
(approval.RequestedByUserId == userId ||
|
||||||
(!string.IsNullOrEmpty(normalizedEmail) && approval.ReviewerEmail.ToUpper() == normalizedEmail)))
|
(!string.IsNullOrEmpty(normalizedEmail) && EF.Functions.ILike(approval.ReviewerEmail, normalizedEmail))))
|
||||||
.Join(
|
.Join(
|
||||||
dbContext.ContentItems,
|
dbContext.ContentItems,
|
||||||
approval => approval.ContentItemId,
|
approval => approval.ContentItemId,
|
||||||
@@ -91,7 +91,7 @@ public class CalendarExportFeedService(AppDbContext dbContext, CalendarExportFee
|
|||||||
appBaseUrl))
|
appBaseUrl))
|
||||||
.ToListAsync(ct));
|
.ToListAsync(ct));
|
||||||
|
|
||||||
return feedBuilder.Build("Socialize my work", events);
|
return CalendarExportFeedBuilder.Build("Socialize my work", events);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CalendarExportFeedEvent ToContentFeedEvent(
|
private static CalendarExportFeedEvent ToContentFeedEvent(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System.Security.Cryptography;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||||
|
|
||||||
public static class CalendarExportFeedTokenService
|
internal static class CalendarExportFeedTokenService
|
||||||
{
|
{
|
||||||
public static string GenerateToken()
|
public static string GenerateToken()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||||
|
|
||||||
public sealed class CalendarImportBackgroundService(
|
internal sealed class CalendarImportBackgroundService(
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
ILogger<CalendarImportBackgroundService> logger)
|
ILogger<CalendarImportBackgroundService> logger)
|
||||||
: BackgroundService
|
: BackgroundService
|
||||||
@@ -23,12 +23,15 @@ public sealed class CalendarImportBackgroundService(
|
|||||||
CalendarImportSyncService syncService = scope.ServiceProvider.GetRequiredService<CalendarImportSyncService>();
|
CalendarImportSyncService syncService = scope.ServiceProvider.GetRequiredService<CalendarImportSyncService>();
|
||||||
await syncService.RefreshDueSourcesAsync(stoppingToken);
|
await syncService.RefreshDueSourcesAsync(stoppingToken);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
catch (OperationCanceledException ex) when (stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
|
logger.LogDebug(ex, "Calendar import background sync stopped.");
|
||||||
}
|
}
|
||||||
|
#pragma warning disable CA1031 // Background service should log and continue after unexpected sync failures.
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
logger.LogError(ex, "Calendar import background sync failed.");
|
logger.LogError(ex, "Calendar import background sync failed.");
|
||||||
}
|
}
|
||||||
|
#pragma warning restore CA1031
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||||
|
|
||||||
public sealed class CalendarImportSyncService(
|
#pragma warning disable S1075 // Supplemental observance identifiers intentionally use stable URI-like values.
|
||||||
|
|
||||||
|
internal sealed class CalendarImportSyncService(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory)
|
||||||
IcsCalendarParser parser)
|
|
||||||
{
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonSerializerOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
public async Task RefreshSourceAsync(Guid sourceId, CancellationToken ct)
|
public async Task RefreshSourceAsync(Guid sourceId, CancellationToken ct)
|
||||||
{
|
{
|
||||||
CalendarSource? source = await dbContext.CalendarSources
|
CalendarSource? source = await dbContext.CalendarSources
|
||||||
@@ -115,7 +119,7 @@ public sealed class CalendarImportSyncService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IReadOnlyCollection<ParsedCalendarEvent>> GetParsedEventsAsync(
|
private static async Task<IReadOnlyCollection<ParsedCalendarEvent>> GetParsedEventsAsync(
|
||||||
HttpClient httpClient,
|
HttpClient httpClient,
|
||||||
string sourceUrl,
|
string sourceUrl,
|
||||||
DateOnly rangeStart,
|
DateOnly rangeStart,
|
||||||
@@ -127,8 +131,8 @@ public sealed class CalendarImportSyncService(
|
|||||||
return await GetNagerEventsAsync(httpClient, sourceUrl, countryCode!, rangeStart, rangeEnd, ct);
|
return await GetNagerEventsAsync(httpClient, sourceUrl, countryCode!, rangeStart, rangeEnd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
string content = await httpClient.GetStringAsync(sourceUrl, ct);
|
string content = await httpClient.GetStringAsync(new Uri(sourceUrl), ct);
|
||||||
return parser.Parse(content, rangeStart, rangeEnd);
|
return IcsCalendarParser.Parse(content, rangeStart, rangeEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IReadOnlyCollection<ParsedCalendarEvent>> GetNagerEventsAsync(
|
private static async Task<IReadOnlyCollection<ParsedCalendarEvent>> GetNagerEventsAsync(
|
||||||
@@ -143,14 +147,12 @@ public sealed class CalendarImportSyncService(
|
|||||||
for (int year = rangeStart.Year; year <= rangeEnd.Year; year++)
|
for (int year = rangeStart.Year; year <= rangeEnd.Year; year++)
|
||||||
{
|
{
|
||||||
string yearUrl = BuildNagerYearUrl(sourceUrl, countryCode, year);
|
string yearUrl = BuildNagerYearUrl(sourceUrl, countryCode, year);
|
||||||
string json = await httpClient.GetStringAsync(yearUrl, ct);
|
string json = await httpClient.GetStringAsync(new Uri(yearUrl), ct);
|
||||||
NagerHoliday[] holidays = JsonSerializer.Deserialize<NagerHoliday[]>(
|
NagerHoliday[] holidays = JsonSerializer.Deserialize<NagerHoliday[]>(json, JsonSerializerOptions) ?? [];
|
||||||
json,
|
|
||||||
new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? [];
|
|
||||||
|
|
||||||
foreach (NagerHoliday holiday in holidays)
|
foreach (NagerHoliday holiday in holidays)
|
||||||
{
|
{
|
||||||
if (!DateOnly.TryParse(holiday.Date, out DateOnly date) ||
|
if (!DateOnly.TryParse(holiday.Date, CultureInfo.InvariantCulture, out DateOnly date) ||
|
||||||
date < rangeStart ||
|
date < rangeStart ||
|
||||||
date > rangeEnd)
|
date > rangeEnd)
|
||||||
{
|
{
|
||||||
@@ -283,7 +285,7 @@ public sealed class CalendarImportSyncService(
|
|||||||
private static string NormalizeUidPart(string? value)
|
private static string NormalizeUidPart(string? value)
|
||||||
{
|
{
|
||||||
return new string((value ?? "holiday")
|
return new string((value ?? "holiday")
|
||||||
.ToLowerInvariant()
|
.ToUpperInvariant()
|
||||||
.Select(character => char.IsLetterOrDigit(character) ? character : '-')
|
.Select(character => char.IsLetterOrDigit(character) ? character : '-')
|
||||||
.ToArray())
|
.ToArray())
|
||||||
.Trim('-');
|
.Trim('-');
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||||
|
|
||||||
public static class CalendarSourceScopes
|
internal static class CalendarSourceScopes
|
||||||
{
|
{
|
||||||
public const string Organization = "Organization";
|
public const string Organization = "Organization";
|
||||||
public const string Workspace = "Workspace";
|
public const string Workspace = "Workspace";
|
||||||
public const string User = "User";
|
public const string User = "User";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class CalendarSourceInheritanceModes
|
internal static class CalendarSourceInheritanceModes
|
||||||
{
|
{
|
||||||
public const string Required = "Required";
|
public const string Required = "Required";
|
||||||
public const string Optional = "Optional";
|
public const string Optional = "Optional";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using Socialize.Api.Modules.CalendarIntegrations.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||||
|
|
||||||
public static class CalendarSourceRules
|
internal static class CalendarSourceRules
|
||||||
{
|
{
|
||||||
public static readonly string[] SupportedScopes =
|
public static readonly string[] SupportedScopes =
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||||
|
|
||||||
public record ParsedCalendarEvent(
|
internal record ParsedCalendarEvent(
|
||||||
string SourceEventUid,
|
string SourceEventUid,
|
||||||
string Title,
|
string Title,
|
||||||
string? Description,
|
string? Description,
|
||||||
@@ -39,9 +40,9 @@ internal sealed record IcsRawEvent(
|
|||||||
string? SourceUrl,
|
string? SourceUrl,
|
||||||
DateTimeOffset? LastModifiedAt);
|
DateTimeOffset? LastModifiedAt);
|
||||||
|
|
||||||
public sealed class IcsCalendarParser
|
internal static class IcsCalendarParser
|
||||||
{
|
{
|
||||||
public IReadOnlyCollection<ParsedCalendarEvent> Parse(
|
public static IReadOnlyCollection<ParsedCalendarEvent> Parse(
|
||||||
string content,
|
string content,
|
||||||
DateOnly rangeStart,
|
DateOnly rangeStart,
|
||||||
DateOnly rangeEnd)
|
DateOnly rangeEnd)
|
||||||
@@ -63,10 +64,12 @@ public sealed class IcsCalendarParser
|
|||||||
private static IEnumerable<IcsRawEvent> ReadRawEvents(string content)
|
private static IEnumerable<IcsRawEvent> ReadRawEvents(string content)
|
||||||
{
|
{
|
||||||
List<string> lines = UnfoldLines(content).ToList();
|
List<string> lines = UnfoldLines(content).ToList();
|
||||||
for (int index = 0; index < lines.Count; index++)
|
int index = 0;
|
||||||
|
while (index < lines.Count)
|
||||||
{
|
{
|
||||||
if (!lines[index].Equals("BEGIN:VEVENT", StringComparison.OrdinalIgnoreCase))
|
if (!lines[index].Equals("BEGIN:VEVENT", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
index++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +77,10 @@ public sealed class IcsCalendarParser
|
|||||||
new(StringComparer.OrdinalIgnoreCase);
|
new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
for (; index < lines.Count && !lines[index].Equals("END:VEVENT", StringComparison.OrdinalIgnoreCase); index++)
|
while (index < lines.Count && !lines[index].Equals("END:VEVENT", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
ParseProperty(lines[index], properties);
|
ParseProperty(lines[index], properties);
|
||||||
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryGetFirst(properties, "DTSTART", out var startProperty))
|
if (!TryGetFirst(properties, "DTSTART", out var startProperty))
|
||||||
@@ -105,32 +109,34 @@ public sealed class IcsCalendarParser
|
|||||||
TryGetFirst(properties, "LAST-MODIFIED", out var lastModified)
|
TryGetFirst(properties, "LAST-MODIFIED", out var lastModified)
|
||||||
? ParseDateTimeValue(lastModified.Value, lastModified.Parameters).UtcDateTime
|
? ParseDateTimeValue(lastModified.Value, lastModified.Parameters).UtcDateTime
|
||||||
: null);
|
: null);
|
||||||
|
|
||||||
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> UnfoldLines(string content)
|
private static IEnumerable<string> UnfoldLines(string content)
|
||||||
{
|
{
|
||||||
string? current = null;
|
StringBuilder? current = null;
|
||||||
using StringReader reader = new(content.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n'));
|
using StringReader reader = new(content.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n'));
|
||||||
while (reader.ReadLine() is { } line)
|
while (reader.ReadLine() is { } line)
|
||||||
{
|
{
|
||||||
if ((line.StartsWith(' ') || line.StartsWith('\t')) && current is not null)
|
if ((line.StartsWith(' ') || line.StartsWith('\t')) && current is not null)
|
||||||
{
|
{
|
||||||
current += line[1..];
|
current.Append(line[1..]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current is not null)
|
if (current is not null)
|
||||||
{
|
{
|
||||||
yield return current;
|
yield return current.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
current = line;
|
current = new StringBuilder(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current is not null)
|
if (current is not null)
|
||||||
{
|
{
|
||||||
yield return current;
|
yield return current.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +315,7 @@ public sealed class IcsCalendarParser
|
|||||||
return TimeSpan.Zero;
|
return TimeSpan.Zero;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyCollection<DateOnly> ExpandStartDates(
|
private static List<DateOnly> ExpandStartDates(
|
||||||
IcsRawEvent rawEvent,
|
IcsRawEvent rawEvent,
|
||||||
DateOnly rangeStart,
|
DateOnly rangeStart,
|
||||||
DateOnly rangeEnd)
|
DateOnly rangeEnd)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Campaigns.Data;
|
namespace Socialize.Api.Modules.Campaigns.Data;
|
||||||
|
|
||||||
public class Campaign
|
internal class Campaign
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Modules.Clients.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Campaigns.Data;
|
namespace Socialize.Api.Modules.Campaigns.Data;
|
||||||
|
|
||||||
public static class CampaignModelConfiguration
|
internal static class CampaignModelConfiguration
|
||||||
{
|
{
|
||||||
public static ModelBuilder ConfigureCampaignsModule(this ModelBuilder modelBuilder)
|
public static ModelBuilder ConfigureCampaignsModule(this ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -20,6 +22,14 @@ public static class CampaignModelConfiguration
|
|||||||
campaign.HasIndex(x => new { x.ClientId, x.Name }).IsUnique();
|
campaign.HasIndex(x => new { x.ClientId, x.Name }).IsUnique();
|
||||||
campaign.HasIndex(x => x.WorkspaceId);
|
campaign.HasIndex(x => x.WorkspaceId);
|
||||||
campaign.HasIndex(x => x.ClientId);
|
campaign.HasIndex(x => x.ClientId);
|
||||||
|
campaign.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
campaign.HasOne<Client>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ClientId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
return modelBuilder;
|
return modelBuilder;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Socialize.Api.Modules.Campaigns.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Campaigns.Handlers;
|
namespace Socialize.Api.Modules.Campaigns.Handlers;
|
||||||
|
|
||||||
public record CreateCampaignRequest(
|
internal record CreateCampaignRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ClientId,
|
Guid ClientId,
|
||||||
string Name,
|
string Name,
|
||||||
@@ -15,7 +15,7 @@ public record CreateCampaignRequest(
|
|||||||
string? Description,
|
string? Description,
|
||||||
string? Notes);
|
string? Notes);
|
||||||
|
|
||||||
public class CreateCampaignRequestValidator
|
internal class CreateCampaignRequestValidator
|
||||||
: Validator<CreateCampaignRequest>
|
: Validator<CreateCampaignRequest>
|
||||||
{
|
{
|
||||||
public CreateCampaignRequestValidator()
|
public CreateCampaignRequestValidator()
|
||||||
@@ -32,7 +32,7 @@ public class CreateCampaignRequestValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateCampaignHandler(
|
internal class CreateCampaignHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<CreateCampaignRequest, CampaignDto>
|
: Endpoint<CreateCampaignRequest, CampaignDto>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ using Socialize.Api.Modules.Campaigns.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Campaigns.Handlers;
|
namespace Socialize.Api.Modules.Campaigns.Handlers;
|
||||||
|
|
||||||
public record GetCampaignsRequest(Guid? WorkspaceId, Guid? ClientId);
|
internal record GetCampaignsRequest(Guid? WorkspaceId, Guid? ClientId);
|
||||||
|
|
||||||
public record CampaignDto(
|
internal record CampaignDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ClientId,
|
Guid ClientId,
|
||||||
@@ -19,7 +19,7 @@ public record CampaignDto(
|
|||||||
DateTimeOffset StartDate,
|
DateTimeOffset StartDate,
|
||||||
DateTimeOffset EndDate);
|
DateTimeOffset EndDate);
|
||||||
|
|
||||||
public class GetCampaignsHandler(
|
internal class GetCampaignsHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<GetCampaignsRequest, IReadOnlyCollection<CampaignDto>>
|
: Endpoint<GetCampaignsRequest, IReadOnlyCollection<CampaignDto>>
|
||||||
@@ -34,7 +34,7 @@ public class GetCampaignsHandler(
|
|||||||
{
|
{
|
||||||
IQueryable<Campaign> query = dbContext.Campaigns.AsQueryable();
|
IQueryable<Campaign> query = dbContext.Campaigns.AsQueryable();
|
||||||
|
|
||||||
if (!accessScopeService.IsManager(User))
|
if (!AccessScopeService.IsManager(User))
|
||||||
{
|
{
|
||||||
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||||
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using Socialize.Api.Modules.Campaigns.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Campaigns;
|
namespace Socialize.Api.Modules.Campaigns;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class ModuleRegistration
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder AddCampaignsModule(
|
public static WebApplicationBuilder AddCampaignsModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Channels.Data;
|
namespace Socialize.Api.Modules.Channels.Data;
|
||||||
|
|
||||||
public class Channel
|
internal class Channel
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Channels.Data;
|
namespace Socialize.Api.Modules.Channels.Data;
|
||||||
|
|
||||||
public static class ChannelModelConfiguration
|
internal static class ChannelModelConfiguration
|
||||||
{
|
{
|
||||||
public static ModelBuilder ConfigureChannelsModule(this ModelBuilder modelBuilder)
|
public static ModelBuilder ConfigureChannelsModule(this ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -19,6 +20,10 @@ public static class ChannelModelConfiguration
|
|||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
channel.HasIndex(x => x.WorkspaceId);
|
channel.HasIndex(x => x.WorkspaceId);
|
||||||
channel.HasIndex(x => new { x.WorkspaceId, x.Network, x.Name }).IsUnique();
|
channel.HasIndex(x => new { x.WorkspaceId, x.Network, x.Name }).IsUnique();
|
||||||
|
channel.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
return modelBuilder;
|
return modelBuilder;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Channels.Handlers;
|
namespace Socialize.Api.Modules.Channels.Handlers;
|
||||||
|
|
||||||
public record ChannelDto(
|
internal record ChannelDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
string Name,
|
string Name,
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ using Socialize.Api.Modules.Channels.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Channels.Handlers;
|
namespace Socialize.Api.Modules.Channels.Handlers;
|
||||||
|
|
||||||
public record CreateChannelRequest(
|
internal record CreateChannelRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
string Name,
|
string Name,
|
||||||
string Network,
|
string Network,
|
||||||
string? Handle,
|
string? Handle,
|
||||||
string? ExternalUrl);
|
string? ExternalUrl);
|
||||||
|
|
||||||
public class CreateChannelRequestValidator
|
internal class CreateChannelRequestValidator
|
||||||
: Validator<CreateChannelRequest>
|
: Validator<CreateChannelRequest>
|
||||||
{
|
{
|
||||||
private static readonly string[] AllowedNetworks =
|
private static readonly string[] AllowedNetworks =
|
||||||
@@ -39,7 +39,7 @@ public class CreateChannelRequestValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateChannelHandler(
|
internal class CreateChannelHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<CreateChannelRequest, ChannelDto>
|
: Endpoint<CreateChannelRequest, ChannelDto>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ using Socialize.Api.Modules.Channels.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Channels.Handlers;
|
namespace Socialize.Api.Modules.Channels.Handlers;
|
||||||
|
|
||||||
public record GetChannelsRequest(Guid? WorkspaceId);
|
internal record GetChannelsRequest(Guid? WorkspaceId);
|
||||||
|
|
||||||
public class GetChannelsHandler(
|
internal class GetChannelsHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<GetChannelsRequest, IReadOnlyCollection<ChannelDto>>
|
: Endpoint<GetChannelsRequest, IReadOnlyCollection<ChannelDto>>
|
||||||
@@ -23,7 +23,7 @@ public class GetChannelsHandler(
|
|||||||
{
|
{
|
||||||
IQueryable<Channel> query = dbContext.Channels.AsQueryable();
|
IQueryable<Channel> query = dbContext.Channels.AsQueryable();
|
||||||
|
|
||||||
if (!accessScopeService.IsManager(User))
|
if (!AccessScopeService.IsManager(User))
|
||||||
{
|
{
|
||||||
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||||
query = query.Where(channel => workspaceScopeIds.Contains(channel.WorkspaceId));
|
query = query.Where(channel => workspaceScopeIds.Contains(channel.WorkspaceId));
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Channels;
|
namespace Socialize.Api.Modules.Channels;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class ModuleRegistration
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder AddChannelsModule(
|
public static WebApplicationBuilder AddChannelsModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Clients.Data;
|
namespace Socialize.Api.Modules.Clients.Data;
|
||||||
|
|
||||||
public class Client
|
internal class Client
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Clients.Data;
|
namespace Socialize.Api.Modules.Clients.Data;
|
||||||
|
|
||||||
public static class ClientModelConfiguration
|
internal static class ClientModelConfiguration
|
||||||
{
|
{
|
||||||
public static ModelBuilder ConfigureClientsModule(this ModelBuilder modelBuilder)
|
public static ModelBuilder ConfigureClientsModule(this ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -21,6 +22,10 @@ public static class ClientModelConfiguration
|
|||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
client.HasIndex(x => new { x.WorkspaceId, x.Name }).IsUnique();
|
client.HasIndex(x => new { x.WorkspaceId, x.Name }).IsUnique();
|
||||||
client.HasIndex(x => x.WorkspaceId);
|
client.HasIndex(x => x.WorkspaceId);
|
||||||
|
client.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
return modelBuilder;
|
return modelBuilder;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user