Compare commits
27 Commits
664eb07201
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c40653b2b7 | |||
| f240d32ce6 | |||
| 4775e35b3c | |||
| a7535d460d | |||
| db344eebac | |||
| 9699c4d55c | |||
| c183626a7a | |||
| 5db182dda9 | |||
| 6296a91c3d | |||
| 91b7f96fdb | |||
| 88c4c23ce1 | |||
| a96b3c897c | |||
| a437bfcfc3 | |||
| b7b282a71a | |||
| 6083797eb1 | |||
| ecbd3daa1b | |||
| b66c10b681 | |||
| c49f03ec06 | |||
| 23ae78f6e1 | |||
| 0d4188b64e | |||
| 78a7517de7 | |||
| 244be555f9 | |||
| 6e658b8215 | |||
| f6c351c31e | |||
| 5baacbceea | |||
| feef8cbafd | |||
| b7379cf823 |
56
.gitea/workflows/deploy-socialize.yml
Normal file
56
.gitea/workflows/deploy-socialize.yml
Normal file
@@ -0,0 +1,56 @@
|
||||
name: deploy-socialize
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
image:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check repository hygiene
|
||||
run: ./scripts/check-repo-hygiene.sh
|
||||
- name: Install Docker CLI
|
||||
run: apt-get update && apt-get install -y docker.io
|
||||
- name: Login to Gitea container registry
|
||||
env:
|
||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: printf '%s' "$REGISTRY_PASSWORD" | docker login git.mapachotes.com -u "$REGISTRY_USER" --password-stdin
|
||||
- name: Build images
|
||||
run: |
|
||||
docker build \
|
||||
-t git.mapachotes.com/jbourdon/socialize-api:${{ gitea.sha }} \
|
||||
-t git.mapachotes.com/jbourdon/socialize-api:latest \
|
||||
-f backend/src/Socialize.Api/Dockerfile .
|
||||
docker build \
|
||||
--build-arg VITE_API_URL=/api \
|
||||
-t git.mapachotes.com/jbourdon/socialize-web:${{ gitea.sha }} \
|
||||
-t git.mapachotes.com/jbourdon/socialize-web:latest \
|
||||
-f frontend/Dockerfile .
|
||||
- name: Push images
|
||||
run: |
|
||||
docker push git.mapachotes.com/jbourdon/socialize-api:${{ gitea.sha }}
|
||||
docker push git.mapachotes.com/jbourdon/socialize-api:latest
|
||||
docker push git.mapachotes.com/jbourdon/socialize-web:${{ gitea.sha }}
|
||||
docker push git.mapachotes.com/jbourdon/socialize-web:latest
|
||||
|
||||
deploy:
|
||||
needs: image
|
||||
runs-on: bookworm
|
||||
steps:
|
||||
- name: Install SSH client
|
||||
run: apt-get update && apt-get install -y openssh-client
|
||||
- name: Deploy on sobina
|
||||
env:
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
|
||||
DEPLOY_SSH_PRIVATE_KEY_B64: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY_B64 }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s' "$DEPLOY_SSH_PRIVATE_KEY_B64" | base64 -d > ~/.ssh/deploy_key
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
'cd /srv/prod/socialize && ./deploy.sh'
|
||||
39
.github/workflows/backend-ci.yml
vendored
39
.github/workflows/backend-ci.yml
vendored
@@ -1,39 +0,0 @@
|
||||
name: Backend CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
AZURE_WEBAPP_NAME: hutopy-backend-api
|
||||
DOTNET_VERSION: '10.0.x'
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: dev
|
||||
steps:
|
||||
|
||||
# Checkout the repository
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Setup .NET Core
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
# Run dotnet publish
|
||||
- name: dotnet build and publish
|
||||
run: |
|
||||
cd backend
|
||||
dotnet publish --configuration Release --artifacts-path ./publish/ Socialize.slnx
|
||||
|
||||
# Deploy to Azure WebApp
|
||||
- name: Deploy to Azure WebApp
|
||||
uses: azure/webapps-deploy@v2
|
||||
with:
|
||||
app-name: ${{ env.AZURE_WEBAPP_NAME }}
|
||||
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
|
||||
package: './backend/publish/publish/Socialize.Api/release/'
|
||||
38
.github/workflows/frontend-ci.yml
vendored
38
.github/workflows/frontend-ci.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: Frontend CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
AZURE_SWA_NAME: hutopy-portal
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
# Checkout the repository
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# Npm install
|
||||
- name: npm install
|
||||
run: |
|
||||
cd frontend
|
||||
npm install
|
||||
|
||||
# Npm run build
|
||||
- name: npm run build
|
||||
run: |
|
||||
cd frontend
|
||||
npm run build
|
||||
|
||||
# Deploy to Azure SWA
|
||||
- name: Deploy to Azure SWA
|
||||
uses: azure/static-web-apps-deploy@v1
|
||||
with:
|
||||
action: "upload"
|
||||
app_location: 'frontend'
|
||||
output_location: 'dist'
|
||||
azure_static_web_apps_api_token: ${{ secrets.AZURE_SWA_TOKEN }}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -22,6 +22,10 @@ Thumbs.db
|
||||
# .NET
|
||||
bin/
|
||||
obj/
|
||||
**/[Bb]in/
|
||||
**/[Oo]bj/
|
||||
**/[Bb]in[\\]*
|
||||
**/[Oo]bj[\\]*
|
||||
TestResults/
|
||||
|
||||
# Node
|
||||
|
||||
82
backend/.github/workflows/build.yml
vendored
82
backend/.github/workflows/build.yml
vendored
@@ -1,82 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- '.scripts/**'
|
||||
- .gitignore
|
||||
- CODE_OF_CONDUCT.md
|
||||
- LICENSE
|
||||
- README.md
|
||||
|
||||
workflow_call:
|
||||
inputs:
|
||||
build-artifacts:
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
name: Checkout code
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
|
||||
- name: Restore solution
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build solution
|
||||
run: dotnet build --no-restore --configuration Release
|
||||
|
||||
- name: Test solution
|
||||
run: dotnet test --no-build --configuration Release --filter "FullyQualifiedName!~AcceptanceTests"
|
||||
|
||||
- name: Publish website
|
||||
if: ${{ inputs.build-artifacts == true }}
|
||||
run: |
|
||||
dotnet publish --configuration Release --runtime win-x86 --self-contained --output ./publish
|
||||
cd publish
|
||||
zip -r ./publish.zip .
|
||||
working-directory: ./src/Web/
|
||||
|
||||
- name: Upload website artifact (website)
|
||||
if: ${{ inputs.build-artifacts == true }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: website
|
||||
path: ./src/Web/publish/publish.zip
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Create EF Core migrations bundle
|
||||
if: ${{ inputs.build-artifacts == true }}
|
||||
run: |
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install dotnet-ef
|
||||
dotnet ef migrations bundle --configuration Release -p ./src/Infrastructure/ -s ./src/Web/ -o efbundle.exe
|
||||
zip -r ./efbundle.zip efbundle.exe
|
||||
env:
|
||||
SkipNSwag: True
|
||||
|
||||
- name: Upload EF Core migrations bundle artifact (efbundle)
|
||||
if: ${{ inputs.build-artifacts == true }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: efbundle
|
||||
path: ./efbundle.zip
|
||||
if-no-files-found: error
|
||||
42
backend/.github/workflows/cicd.yml
vendored
42
backend/.github/workflows/cicd.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: CICD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
paths-ignore:
|
||||
- .gitignore
|
||||
- CODE_OF_CONDUCT.md
|
||||
- LICENSE
|
||||
- README.md
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
uses: ./.github/workflows/build.yml
|
||||
with:
|
||||
build-artifacts: true
|
||||
|
||||
deploy-development:
|
||||
uses: ./.github/workflows/deploy.yml
|
||||
secrets: inherit
|
||||
needs: [ build ]
|
||||
with:
|
||||
environmentName: Development
|
||||
|
||||
deploy-staging:
|
||||
uses: ./.github/workflows/deploy.yml
|
||||
secrets: inherit
|
||||
needs: [ deploy-development ]
|
||||
with:
|
||||
environmentName: Staging
|
||||
|
||||
deploy-production:
|
||||
uses: ./.github/workflows/deploy.yml
|
||||
secrets: inherit
|
||||
needs: [ deploy-staging ]
|
||||
with:
|
||||
environmentName: Production
|
||||
107
backend/.github/workflows/deploy.yml
vendored
107
backend/.github/workflows/deploy.yml
vendored
@@ -1,107 +0,0 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
environmentName:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environmentName }}
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
name: Checkout code
|
||||
|
||||
- uses: azure/login@v1
|
||||
name: Login to Azure
|
||||
with:
|
||||
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- if: inputs.environmentName == 'Development'
|
||||
uses: azure/arm-deploy@v1
|
||||
name: Run preflight validation
|
||||
with:
|
||||
deploymentName: ${{ github.run_number }}
|
||||
resourceGroupName: ${{ vars.AZURE_RESOURCE_GROUP_NAME }}
|
||||
template: ./.azure/bicep/main.bicep
|
||||
parameters: >
|
||||
environmentName=${{ inputs.environmentName }}
|
||||
sqlAdministratorUsername=${{ vars.AZURE_SQL_ADMINISTRATOR_USERNAME }}
|
||||
sqlAdministratorPassword=${{ secrets.AZURE_SQL_ADMINISTRATOR_PASSWORD }}
|
||||
projectName=${{ vars.PROJECT_NAME }}
|
||||
deploymentMode: Validate
|
||||
|
||||
- if: inputs.environmentName != 'Development'
|
||||
uses: azure/arm-deploy@v1
|
||||
name: Run what-if
|
||||
with:
|
||||
failOnStdErr: false
|
||||
resourceGroupName: ${{ vars.AZURE_RESOURCE_GROUP_NAME }}
|
||||
template: ./.azure/bicep/main.bicep
|
||||
parameters: >
|
||||
environmentName=${{ inputs.environmentName }}
|
||||
sqlAdministratorUsername=${{ vars.AZURE_SQL_ADMINISTRATOR_USERNAME }}
|
||||
sqlAdministratorPassword=${{ secrets.AZURE_SQL_ADMINISTRATOR_PASSWORD }}
|
||||
projectName=${{ vars.PROJECT_NAME }}
|
||||
additionalArguments: --what-if
|
||||
|
||||
deploy:
|
||||
needs: [ validate ]
|
||||
runs-on: ubuntu-latest
|
||||
environment: ${{ inputs.environmentName }}
|
||||
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
name: Checkout code
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
name: Download artifacts
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
|
||||
- uses: azure/login@v1
|
||||
name: Login to Azure
|
||||
with:
|
||||
client-id: ${{ vars.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- uses: azure/arm-deploy@v1
|
||||
id: deploy
|
||||
name: Deploy infrastructure
|
||||
with:
|
||||
failOnStdErr: false
|
||||
deploymentName: ${{ github.run_number }}
|
||||
resourceGroupName: ${{ vars.AZURE_RESOURCE_GROUP_NAME }}
|
||||
template: ./.azure/bicep/main.bicep
|
||||
parameters: >
|
||||
environmentName=${{ inputs.environmentName }}
|
||||
sqlAdministratorUsername=${{ vars.AZURE_SQL_ADMINISTRATOR_USERNAME }}
|
||||
sqlAdministratorPassword=${{ secrets.AZURE_SQL_ADMINISTRATOR_PASSWORD }}
|
||||
projectName=${{ vars.PROJECT_NAME }}
|
||||
|
||||
- name: Initialise database
|
||||
run: |
|
||||
unzip -o ./efbundle/efbundle.zip
|
||||
echo '{ "ConnectionStrings": { "DefaultConnection": "" } }' > appsettings.json
|
||||
./efbundle.exe --connection "Server=${{ steps.deploy.outputs.sqlServerFullyQualifiedDomainName }};Initial Catalog=${{ steps.deploy.outputs.sqlDatabaseName }};Persist Security Info=False;User ID=${{ vars.AZURE_SQL_ADMINISTRATOR_USERNAME }};Password=${{ secrets.AZURE_SQL_ADMINISTRATOR_PASSWORD }};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" --verbose
|
||||
|
||||
- uses: azure/webapps-deploy@v2
|
||||
name: Deploy website
|
||||
with:
|
||||
app-name: ${{ steps.deploy.outputs.appServiceAppName }}
|
||||
package: website/publish.zip
|
||||
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.Channels.Data;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
@@ -9,6 +10,7 @@ using Socialize.Api.Modules.Feedback.Data;
|
||||
using Socialize.Api.Modules.Identity.Data;
|
||||
using Socialize.Api.Modules.Notifications.Data;
|
||||
using Socialize.Api.Modules.Campaigns.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using Socialize.Api.Modules.Organizations.Data;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
|
||||
@@ -22,10 +24,12 @@ public class AppDbContext(
|
||||
public DbSet<OrganizationMembership> OrganizationMemberships => Set<OrganizationMembership>();
|
||||
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
||||
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
||||
public DbSet<Channel> Channels => Set<Channel>();
|
||||
public DbSet<Client> Clients => Set<Client>();
|
||||
public DbSet<Campaign> Campaigns => Set<Campaign>();
|
||||
public DbSet<ContentItem> ContentItems => Set<ContentItem>();
|
||||
public DbSet<ContentItemRevision> ContentItemRevisions => Set<ContentItemRevision>();
|
||||
public DbSet<ContentItemActivityEntry> ContentItemActivityEntries => Set<ContentItemActivityEntry>();
|
||||
public DbSet<Asset> Assets => Set<Asset>();
|
||||
public DbSet<AssetRevision> AssetRevisions => Set<AssetRevision>();
|
||||
public DbSet<Comment> Comments => Set<Comment>();
|
||||
@@ -39,6 +43,10 @@ public class AppDbContext(
|
||||
public DbSet<FeedbackScreenshot> FeedbackScreenshots => Set<FeedbackScreenshot>();
|
||||
public DbSet<FeedbackComment> FeedbackComments => Set<FeedbackComment>();
|
||||
public DbSet<FeedbackActivityEntry> FeedbackActivityEntries => Set<FeedbackActivityEntry>();
|
||||
public DbSet<CalendarSource> CalendarSources => Set<CalendarSource>();
|
||||
public DbSet<CalendarCatalogEntry> CalendarCatalogEntries => Set<CalendarCatalogEntry>();
|
||||
public DbSet<CalendarEvent> CalendarEvents => Set<CalendarEvent>();
|
||||
public DbSet<UserCalendarExportFeed> UserCalendarExportFeeds => Set<UserCalendarExportFeed>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
@@ -46,6 +54,7 @@ public class AppDbContext(
|
||||
|
||||
builder.ConfigureOrganizationsModule();
|
||||
builder.ConfigureWorkspacesModule();
|
||||
builder.ConfigureChannelsModule();
|
||||
builder.ConfigureClientsModule();
|
||||
builder.ConfigureCampaignsModule();
|
||||
builder.ConfigureContentItemsModule();
|
||||
@@ -54,5 +63,6 @@ public class AppDbContext(
|
||||
builder.ConfigureApprovalsModule();
|
||||
builder.ConfigureNotificationsModule();
|
||||
builder.ConfigureFeedbackModule();
|
||||
builder.ConfigureCalendarIntegrationsModule();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ internal static class ContainerNames
|
||||
{
|
||||
public const string Users = "users";
|
||||
public const string Clients = "clients";
|
||||
public const string Organizations = "organizations";
|
||||
public const string Workspaces = "workspaces";
|
||||
public const string Creators = "creators";
|
||||
public const string Feedback = "feedback";
|
||||
|
||||
@@ -6,6 +6,7 @@ using Socialize.Api.Modules.Identity.Contracts;
|
||||
using Socialize.Api.Modules.Identity.Data;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Channels.Data;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Clients.Data;
|
||||
@@ -23,10 +24,14 @@ public static class DevelopmentSeedExtensions
|
||||
{
|
||||
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 AtlasWorkspaceId = Guid.Parse("11111111-1111-1111-1111-222222222222");
|
||||
private static readonly Guid ScopedClientId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
||||
private static readonly Guid HiddenClientId = Guid.Parse("22222222-2222-2222-2222-333333333333");
|
||||
private static readonly Guid ScopedCampaignId = Guid.Parse("33333333-3333-3333-3333-333333333333");
|
||||
private static readonly Guid HiddenCampaignId = Guid.Parse("33333333-3333-3333-3333-444444444444");
|
||||
private static readonly Guid LumaInstagramChannelId = Guid.Parse("33333333-3333-3333-3333-000000000001");
|
||||
private static readonly Guid LumaTikTokChannelId = Guid.Parse("33333333-3333-3333-3333-000000000002");
|
||||
private static readonly Guid AtlasInstagramChannelId = Guid.Parse("33333333-3333-3333-3333-000000000003");
|
||||
private static readonly Guid ScopedContentItemId = Guid.Parse("44444444-4444-4444-4444-444444444444");
|
||||
private static readonly Guid HiddenContentItemId = Guid.Parse("44444444-4444-4444-4444-555555555555");
|
||||
private static readonly Guid ScopedAssetId = Guid.Parse("55555555-5555-5555-5555-555555555555");
|
||||
@@ -252,7 +257,7 @@ public static class DevelopmentSeedExtensions
|
||||
dbContext.Organizations.Add(organization);
|
||||
}
|
||||
|
||||
organization.Name = "Northstar Collective";
|
||||
organization.Name = "Northstar Agency";
|
||||
organization.OwnerUserId = managerUserId;
|
||||
|
||||
await UpsertOrganizationMembershipAsync(
|
||||
@@ -309,34 +314,31 @@ public static class DevelopmentSeedExtensions
|
||||
AppDbContext dbContext,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Workspace? workspace = await dbContext.Workspaces
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == WorkspaceId, cancellationToken);
|
||||
if (workspace is null)
|
||||
{
|
||||
workspace = new Workspace
|
||||
{
|
||||
Id = WorkspaceId,
|
||||
Name = string.Empty,
|
||||
Slug = string.Empty,
|
||||
TimeZone = string.Empty,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
dbContext.Workspaces.Add(workspace);
|
||||
}
|
||||
|
||||
workspace.Name = "Northstar Studio";
|
||||
workspace.Slug = "northstar-studio";
|
||||
workspace.OrganizationId = OrganizationId;
|
||||
workspace.OwnerUserId = managerUserId;
|
||||
workspace.TimeZone = "America/Montreal";
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await UpsertWorkspaceAsync(
|
||||
dbContext,
|
||||
WorkspaceId,
|
||||
OrganizationId,
|
||||
managerUserId,
|
||||
"Luma Coffee",
|
||||
"America/Montreal",
|
||||
"/images/seed/luma-coffee-logo.svg",
|
||||
cancellationToken);
|
||||
await UpsertWorkspaceAsync(
|
||||
dbContext,
|
||||
AtlasWorkspaceId,
|
||||
OrganizationId,
|
||||
managerUserId,
|
||||
"Atlas Bakery",
|
||||
"America/Montreal",
|
||||
"/images/seed/atlas-bakery-logo.svg",
|
||||
cancellationToken);
|
||||
|
||||
await UpsertClientAsync(
|
||||
dbContext,
|
||||
ScopedClientId,
|
||||
"Luma Coffee",
|
||||
"Active",
|
||||
"https://images.unsplash.com/photo-1511920170033-f8396924c348?auto=format&fit=crop&w=200&q=80",
|
||||
"/images/seed/luma-coffee-logo.svg",
|
||||
"Sofia Martin",
|
||||
"client@socialize.local",
|
||||
WorkspaceId,
|
||||
@@ -346,10 +348,10 @@ public static class DevelopmentSeedExtensions
|
||||
HiddenClientId,
|
||||
"Atlas Bakery",
|
||||
"Active",
|
||||
"https://images.unsplash.com/photo-1509440159596-0249088772ff?auto=format&fit=crop&w=200&q=80",
|
||||
"/images/seed/atlas-bakery-logo.svg",
|
||||
"Nina Cole",
|
||||
"nina@atlasbakery.test",
|
||||
WorkspaceId,
|
||||
AtlasWorkspaceId,
|
||||
cancellationToken);
|
||||
|
||||
await UpsertCampaignAsync(
|
||||
@@ -367,7 +369,7 @@ public static class DevelopmentSeedExtensions
|
||||
await UpsertCampaignAsync(
|
||||
dbContext,
|
||||
HiddenCampaignId,
|
||||
WorkspaceId,
|
||||
AtlasWorkspaceId,
|
||||
HiddenClientId,
|
||||
"Summer Retention",
|
||||
"Planned",
|
||||
@@ -377,6 +379,34 @@ public static class DevelopmentSeedExtensions
|
||||
"Sequence email and paid social updates together.",
|
||||
cancellationToken);
|
||||
|
||||
await UpsertChannelAsync(
|
||||
dbContext,
|
||||
LumaInstagramChannelId,
|
||||
WorkspaceId,
|
||||
"Luma Coffee Instagram",
|
||||
"Instagram",
|
||||
"@lumacoffee",
|
||||
null,
|
||||
cancellationToken);
|
||||
await UpsertChannelAsync(
|
||||
dbContext,
|
||||
LumaTikTokChannelId,
|
||||
WorkspaceId,
|
||||
"Luma Coffee TikTok",
|
||||
"TikTok",
|
||||
"@lumacoffee",
|
||||
null,
|
||||
cancellationToken);
|
||||
await UpsertChannelAsync(
|
||||
dbContext,
|
||||
AtlasInstagramChannelId,
|
||||
AtlasWorkspaceId,
|
||||
"Atlas Bakery Instagram",
|
||||
"Instagram",
|
||||
"@atlasbakery",
|
||||
null,
|
||||
cancellationToken);
|
||||
|
||||
await UpsertContentItemAsync(
|
||||
dbContext,
|
||||
ScopedContentItemId,
|
||||
@@ -385,7 +415,7 @@ public static class DevelopmentSeedExtensions
|
||||
ScopedCampaignId,
|
||||
"Spring launch hero video",
|
||||
"Fresh seasonal menu launch across Instagram and TikTok.",
|
||||
"Instagram Reel, TikTok",
|
||||
"Luma Coffee Instagram, Luma Coffee TikTok",
|
||||
"In approval",
|
||||
DateTimeOffset.UtcNow.AddDays(3),
|
||||
"v3",
|
||||
@@ -394,22 +424,22 @@ public static class DevelopmentSeedExtensions
|
||||
await UpsertContentItemAsync(
|
||||
dbContext,
|
||||
HiddenContentItemId,
|
||||
WorkspaceId,
|
||||
AtlasWorkspaceId,
|
||||
HiddenClientId,
|
||||
HiddenCampaignId,
|
||||
"Bakery loyalty carousel",
|
||||
"Reward regular customers with a four-card retention carousel.",
|
||||
"Instagram Carousel",
|
||||
"Atlas Bakery Instagram",
|
||||
"Draft",
|
||||
DateTimeOffset.UtcNow.AddDays(10),
|
||||
"v1",
|
||||
1,
|
||||
cancellationToken);
|
||||
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000001"), ScopedContentItemId, 1, "v1", "Spring launch hero video", "Initial draft for the seasonal menu launch.", "Instagram Reel, TikTok", "Initial concept draft.", providerUserId, DateTimeOffset.UtcNow.AddDays(-5), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000002"), ScopedContentItemId, 2, "v2", "Spring launch hero video", "Updated hook and transitions after internal review.", "Instagram Reel, TikTok", "Addressed internal pacing feedback.", providerUserId, DateTimeOffset.UtcNow.AddDays(-3), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000003"), ScopedContentItemId, 3, "v3", "Spring launch hero video", "Fresh seasonal menu launch across Instagram and TikTok.", "Instagram Reel, TikTok", "Client-facing draft after copy cleanup.", providerUserId, DateTimeOffset.UtcNow.AddDays(-1), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000004"), HiddenContentItemId, 1, "v1", "Bakery loyalty carousel", "Reward regular customers with a four-card retention carousel.", "Instagram Carousel", "First draft.", managerUserId, DateTimeOffset.UtcNow.AddDays(-2), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000001"), ScopedContentItemId, 1, "v1", "Spring launch hero video", "Initial draft for the seasonal menu launch.", "Luma Coffee Instagram, Luma Coffee TikTok", "Initial concept draft.", providerUserId, DateTimeOffset.UtcNow.AddDays(-5), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000002"), ScopedContentItemId, 2, "v2", "Spring launch hero video", "Updated hook and transitions after internal review.", "Luma Coffee Instagram, Luma Coffee TikTok", "Addressed internal pacing feedback.", providerUserId, DateTimeOffset.UtcNow.AddDays(-3), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000003"), ScopedContentItemId, 3, "v3", "Spring launch hero video", "Fresh seasonal menu launch across Instagram and TikTok.", "Luma Coffee Instagram, Luma Coffee TikTok", "Client-facing draft after copy cleanup.", providerUserId, DateTimeOffset.UtcNow.AddDays(-1), cancellationToken);
|
||||
await EnsureRevisionAsync(dbContext, Guid.Parse("44444444-4444-4444-4444-000000000004"), HiddenContentItemId, 1, "v1", "Bakery loyalty carousel", "Reward regular customers with a four-card retention carousel.", "Atlas Bakery Instagram", "First draft.", managerUserId, DateTimeOffset.UtcNow.AddDays(-2), cancellationToken);
|
||||
|
||||
Asset? asset = await dbContext.Assets.SingleOrDefaultAsync(candidate => candidate.Id == ScopedAssetId, cancellationToken);
|
||||
if (asset is null)
|
||||
@@ -457,8 +487,6 @@ public static class DevelopmentSeedExtensions
|
||||
comment.AuthorDisplayName = "Sofia Martin";
|
||||
comment.AuthorEmail = "client@socialize.local";
|
||||
comment.Body = "Please tighten the opening three seconds and make the launch CTA more explicit.";
|
||||
comment.IsResolved = false;
|
||||
comment.ResolvedAt = null;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
ApprovalRequest? approvalRequest = await dbContext.ApprovalRequests.SingleOrDefaultAsync(candidate => candidate.Id == ScopedApprovalRequestId, cancellationToken);
|
||||
@@ -537,6 +565,38 @@ public static class DevelopmentSeedExtensions
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task UpsertWorkspaceAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid id,
|
||||
Guid organizationId,
|
||||
Guid ownerUserId,
|
||||
string name,
|
||||
string timeZone,
|
||||
string logoUrl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Workspace? workspace = await dbContext.Workspaces
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
|
||||
if (workspace is null)
|
||||
{
|
||||
workspace = new Workspace
|
||||
{
|
||||
Id = id,
|
||||
Name = string.Empty,
|
||||
TimeZone = string.Empty,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
dbContext.Workspaces.Add(workspace);
|
||||
}
|
||||
|
||||
workspace.Name = name;
|
||||
workspace.OrganizationId = organizationId;
|
||||
workspace.OwnerUserId = ownerUserId;
|
||||
workspace.TimeZone = timeZone;
|
||||
workspace.LogoUrl = logoUrl;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task UpsertClientAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid id,
|
||||
@@ -606,6 +666,37 @@ public static class DevelopmentSeedExtensions
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task UpsertChannelAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid id,
|
||||
Guid workspaceId,
|
||||
string name,
|
||||
string network,
|
||||
string? handle,
|
||||
string? externalUrl,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Channel? channel = await dbContext.Channels.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
|
||||
if (channel is null)
|
||||
{
|
||||
channel = new Channel
|
||||
{
|
||||
Id = id,
|
||||
Name = string.Empty,
|
||||
Network = string.Empty,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
dbContext.Channels.Add(channel);
|
||||
}
|
||||
|
||||
channel.WorkspaceId = workspaceId;
|
||||
channel.Name = name;
|
||||
channel.Network = network;
|
||||
channel.Handle = handle;
|
||||
channel.ExternalUrl = externalUrl;
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task UpsertContentItemAsync(
|
||||
AppDbContext dbContext,
|
||||
Guid id,
|
||||
|
||||
@@ -1,942 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using Socialize.Api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260423061407_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.0")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Approvals.Data.ApprovalDecision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ApprovalRequestId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("DecidedByEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("DecidedByName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid?>("DecidedByUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Decision")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ApprovalRequestId");
|
||||
|
||||
b.ToTable("ApprovalDecisions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Approvals.Data.ApprovalRequest", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AccessToken")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("ContentItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("DueAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("RequestedByUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ReviewerEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("ReviewerName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<DateTimeOffset>("SentAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Stage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContentItemId");
|
||||
|
||||
b.HasIndex("ReviewerEmail");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("ApprovalRequests", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Assets.Data.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AssetType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("ContentItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<int>("CurrentRevisionNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("GoogleDriveFileId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("GoogleDriveLink")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("PreviewUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("SourceType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContentItemId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("Assets", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Assets.Data.AssetRevision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid?>("CreatedByUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("PreviewUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("RevisionNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("SourceReference")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("AssetId", "RevisionNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AssetRevisions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Clients.Data.Client", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PortraitUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("PrimaryContactEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PrimaryContactName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PrimaryContactPortraitUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.HasIndex("WorkspaceId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Clients", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Comments.Data.Comment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("AuthorDisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("AuthorEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid>("AuthorUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Body")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<Guid>("ContentItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid?>("ParentCommentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("ResolvedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContentItemId");
|
||||
|
||||
b.HasIndex("ParentCommentId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("Comments", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.ContentItems.Data.ContentItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ClientId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("CurrentRevisionLabel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<int>("CurrentRevisionNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset?>("DueDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Hashtags")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<Guid>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PublicationMessage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<string>("PublicationTargets")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("ProjectId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("ContentItems", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.ContentItems.Data.ContentItemRevision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ChangeSummary")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<Guid>("ContentItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid?>("CreatedByUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Hashtags")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("PublicationMessage")
|
||||
.IsRequired()
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<string>("PublicationTargets")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("RevisionLabel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<int>("RevisionNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContentItemId");
|
||||
|
||||
b.HasIndex("ContentItemId", "RevisionNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ContentItemRevisions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Identity.Data.Role", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Identity.Data.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Alias")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<DateTime?>("BirthDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("FacebookId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Firstname")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("GoogleId")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Lastname")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("PortraitUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.HasMaxLength(44)
|
||||
.HasColumnType("character varying(44)");
|
||||
|
||||
b.Property<DateTime>("RefreshTokenExpiryTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Notifications.Data.NotificationEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ContentItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("EntityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("EntityType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("MetadataJson")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<DateTimeOffset?>("ReadAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("RecipientEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid?>("RecipientUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContentItemId");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("RecipientUserId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("NotificationEvents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Projects.Data.Project", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ClientId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<DateTimeOffset>("EndDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<DateTimeOffset>("StartDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.HasIndex("ClientId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Projects", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Workspaces.Data.Workspace", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid>("OwnerUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("TimeZone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Workspaces", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Modules.Workspaces.Data.WorkspaceInvite", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid>("InvitedByUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.HasIndex("WorkspaceId", "Email", "Status");
|
||||
|
||||
b.ToTable("WorkspaceInvites", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Modules.Identity.Data.Role", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Modules.Identity.Data.Role", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Modules.Identity.Data.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,657 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApprovalDecisions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ApprovalRequestId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Decision = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Comment = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
DecidedByUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
DecidedByName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
DecidedByEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApprovalDecisions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApprovalRequests",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ContentItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Stage = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
ReviewerName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
ReviewerEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
RequestedByUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DueAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
State = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
AccessToken = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
SentAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
CompletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApprovalRequests", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoles",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUsers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Alias = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
Firstname = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
Lastname = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
BirthDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
Address = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
PortraitUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
GoogleId = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
FacebookId = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
RefreshToken = table.Column<string>(type: "character varying(44)", maxLength: 44, nullable: true),
|
||||
RefreshTokenExpiryTime = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: true),
|
||||
SecurityStamp = table.Column<string>(type: "text", nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumber = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AssetRevisions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AssetId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
RevisionNumber = table.Column<int>(type: "integer", nullable: false),
|
||||
SourceReference = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
PreviewUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
Notes = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
CreatedByUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AssetRevisions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Assets",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ContentItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AssetType = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
SourceType = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
GoogleDriveFileId = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
GoogleDriveLink = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
PreviewUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
CurrentRevisionNumber = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Assets", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Clients",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
PortraitUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
PrimaryContactName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
PrimaryContactEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
PrimaryContactPortraitUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Clients", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Comments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ContentItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ParentCommentId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
AuthorUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AuthorDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
AuthorEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Body = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
|
||||
IsResolved = table.Column<bool>(type: "boolean", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
ResolvedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Comments", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ContentItemRevisions",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ContentItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
RevisionNumber = table.Column<int>(type: "integer", nullable: false),
|
||||
RevisionLabel = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
PublicationMessage = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
|
||||
PublicationTargets = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
Hashtags = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
ChangeSummary = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
CreatedByUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ContentItemRevisions", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ContentItems",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ClientId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
PublicationMessage = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
|
||||
PublicationTargets = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
Hashtags = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
Status = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
DueDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
CurrentRevisionLabel = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
CurrentRevisionNumber = table.Column<int>(type: "integer", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ContentItems", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NotificationEvents",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ContentItemId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
EventType = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
EntityType = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
EntityId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Message = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
RecipientUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
RecipientEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
MetadataJson = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
ReadAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NotificationEvents", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Projects",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ClientId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: true),
|
||||
Notes = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: true),
|
||||
Status = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
StartDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
EndDate = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Projects", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WorkspaceInvites",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Role = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
InvitedByUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WorkspaceInvites", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Workspaces",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
Slug = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
OwnerUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TimeZone = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Workspaces", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
RoleId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserClaims_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserLogins",
|
||||
columns: table => new
|
||||
{
|
||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "text", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
RoleId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserTokens",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Value = table.Column<string>(type: "text", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApprovalDecisions_ApprovalRequestId",
|
||||
table: "ApprovalDecisions",
|
||||
column: "ApprovalRequestId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApprovalRequests_ContentItemId",
|
||||
table: "ApprovalRequests",
|
||||
column: "ContentItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApprovalRequests_ReviewerEmail",
|
||||
table: "ApprovalRequests",
|
||||
column: "ReviewerEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApprovalRequests_WorkspaceId",
|
||||
table: "ApprovalRequests",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
table: "AspNetRoles",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserClaims_UserId",
|
||||
table: "AspNetUserClaims",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserLogins_UserId",
|
||||
table: "AspNetUserLogins",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserRoles_RoleId",
|
||||
table: "AspNetUserRoles",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedEmail");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedUserName",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AssetRevisions_AssetId",
|
||||
table: "AssetRevisions",
|
||||
column: "AssetId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AssetRevisions_AssetId_RevisionNumber",
|
||||
table: "AssetRevisions",
|
||||
columns: new[] { "AssetId", "RevisionNumber" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Assets_ContentItemId",
|
||||
table: "Assets",
|
||||
column: "ContentItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Assets_WorkspaceId",
|
||||
table: "Assets",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Clients_WorkspaceId",
|
||||
table: "Clients",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Clients_WorkspaceId_Name",
|
||||
table: "Clients",
|
||||
columns: new[] { "WorkspaceId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Comments_ContentItemId",
|
||||
table: "Comments",
|
||||
column: "ContentItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Comments_ParentCommentId",
|
||||
table: "Comments",
|
||||
column: "ParentCommentId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Comments_WorkspaceId",
|
||||
table: "Comments",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContentItemRevisions_ContentItemId",
|
||||
table: "ContentItemRevisions",
|
||||
column: "ContentItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContentItemRevisions_ContentItemId_RevisionNumber",
|
||||
table: "ContentItemRevisions",
|
||||
columns: new[] { "ContentItemId", "RevisionNumber" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContentItems_ClientId",
|
||||
table: "ContentItems",
|
||||
column: "ClientId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContentItems_ProjectId",
|
||||
table: "ContentItems",
|
||||
column: "ProjectId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ContentItems_WorkspaceId",
|
||||
table: "ContentItems",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NotificationEvents_ContentItemId",
|
||||
table: "NotificationEvents",
|
||||
column: "ContentItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NotificationEvents_CreatedAt",
|
||||
table: "NotificationEvents",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NotificationEvents_RecipientUserId",
|
||||
table: "NotificationEvents",
|
||||
column: "RecipientUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NotificationEvents_WorkspaceId",
|
||||
table: "NotificationEvents",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Projects_ClientId",
|
||||
table: "Projects",
|
||||
column: "ClientId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Projects_ClientId_Name",
|
||||
table: "Projects",
|
||||
columns: new[] { "ClientId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Projects_WorkspaceId",
|
||||
table: "Projects",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkspaceInvites_WorkspaceId",
|
||||
table: "WorkspaceInvites",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkspaceInvites_WorkspaceId_Email_Status",
|
||||
table: "WorkspaceInvites",
|
||||
columns: new[] { "WorkspaceId", "Email", "Status" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Workspaces_OwnerUserId",
|
||||
table: "Workspaces",
|
||||
column: "OwnerUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Workspaces_Slug",
|
||||
table: "Workspaces",
|
||||
column: "Slug",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApprovalDecisions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApprovalRequests");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoleClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserLogins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AssetRevisions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Assets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Clients");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Comments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ContentItemRevisions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ContentItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "NotificationEvents");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Projects");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "WorkspaceInvites");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Workspaces");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Socialize.Api.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260430054500_AddWorkspaceLogo")]
|
||||
public partial class AddWorkspaceLogo : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "LogoUrl",
|
||||
table: "Workspaces",
|
||||
type: "character varying(2048)",
|
||||
maxLength: 2048,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LogoUrl",
|
||||
table: "Workspaces");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,116 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFeedbackFoundation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeedbackReports",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(8000)", maxLength: 8000, nullable: false),
|
||||
ReporterUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ReporterDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
ReporterEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
SubmittedPath = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
BrowserUserAgent = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
|
||||
ViewportWidth = table.Column<int>(type: "integer", nullable: true),
|
||||
ViewportHeight = table.Column<int>(type: "integer", nullable: true),
|
||||
AppVersion = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
WorkspaceName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ClientId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
ClientName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ProjectId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
ProjectName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ContentItemId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
ContentItemTitle = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
LastActivityAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
CancelledAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
CancelledByUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
CancellationReason = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FeedbackReports", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeedbackTags",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
NormalizedName = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FeedbackTags", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_FeedbackTags_FeedbackReports_FeedbackReportId",
|
||||
column: x => x.FeedbackReportId,
|
||||
principalTable: "FeedbackReports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackReports_LastActivityAt",
|
||||
table: "FeedbackReports",
|
||||
column: "LastActivityAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackReports_ReporterUserId",
|
||||
table: "FeedbackReports",
|
||||
column: "ReporterUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackReports_Status",
|
||||
table: "FeedbackReports",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackReports_Type",
|
||||
table: "FeedbackReports",
|
||||
column: "Type");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackReports_WorkspaceId",
|
||||
table: "FeedbackReports",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackTags_FeedbackReportId_NormalizedName",
|
||||
table: "FeedbackTags",
|
||||
columns: new[] { "FeedbackReportId", "NormalizedName" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackTags_NormalizedName",
|
||||
table: "FeedbackTags",
|
||||
column: "NormalizedName");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeedbackTags");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeedbackReports");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,52 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFeedbackScreenshots : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeedbackScreenshots",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FileName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
ContentType = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
SizeBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||
BlobContainerName = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
BlobName = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FeedbackScreenshots", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_FeedbackScreenshots_FeedbackReports_FeedbackReportId",
|
||||
column: x => x.FeedbackReportId,
|
||||
principalTable: "FeedbackReports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackScreenshots_FeedbackReportId",
|
||||
table: "FeedbackScreenshots",
|
||||
column: "FeedbackReportId",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeedbackScreenshots");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,105 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddFeedbackCommentsActivity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeedbackActivityEntries",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ActorUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ActorDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
ActorEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
ActivityType = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
FromValue = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
ToValue = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: true),
|
||||
Note = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FeedbackActivityEntries", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_FeedbackActivityEntries_FeedbackReports_FeedbackReportId",
|
||||
column: x => x.FeedbackReportId,
|
||||
principalTable: "FeedbackReports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "FeedbackComments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FeedbackReportId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AuthorUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AuthorDisplayName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
AuthorEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
AuthorRole = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Body = table.Column<string>(type: "character varying(8000)", maxLength: 8000, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_FeedbackComments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_FeedbackComments_FeedbackReports_FeedbackReportId",
|
||||
column: x => x.FeedbackReportId,
|
||||
principalTable: "FeedbackReports",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackActivityEntries_ActorUserId",
|
||||
table: "FeedbackActivityEntries",
|
||||
column: "ActorUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackActivityEntries_CreatedAt",
|
||||
table: "FeedbackActivityEntries",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackActivityEntries_FeedbackReportId",
|
||||
table: "FeedbackActivityEntries",
|
||||
column: "FeedbackReportId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackComments_AuthorUserId",
|
||||
table: "FeedbackComments",
|
||||
column: "AuthorUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackComments_CreatedAt",
|
||||
table: "FeedbackComments",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_FeedbackComments_FeedbackReportId",
|
||||
table: "FeedbackComments",
|
||||
column: "FeedbackReportId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeedbackActivityEntries");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "FeedbackComments");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,63 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddWorkspaceApprovalConfiguration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ApprovalMode",
|
||||
table: "Workspaces",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false,
|
||||
defaultValue: "Required");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "LockContentAfterApproval",
|
||||
table: "Workspaces",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "SchedulePostsAutomaticallyOnApproval",
|
||||
table: "Workspaces",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "SendAutomaticApprovalReminders",
|
||||
table: "Workspaces",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ApprovalMode",
|
||||
table: "Workspaces");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "LockContentAfterApproval",
|
||||
table: "Workspaces");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SchedulePostsAutomaticallyOnApproval",
|
||||
table: "Workspaces");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SendAutomaticApprovalReminders",
|
||||
table: "Workspaces");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,51 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddWorkspaceApprovalStepConfiguration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "WorkspaceApprovalStepConfigurations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||
TargetType = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
TargetValue = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
RequiredApproverCount = table.Column<int>(type: "integer", nullable: false, defaultValue: 1),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_WorkspaceApprovalStepConfigurations", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkspaceApprovalStepConfigurations_WorkspaceId",
|
||||
table: "WorkspaceApprovalStepConfigurations",
|
||||
column: "WorkspaceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_WorkspaceApprovalStepConfigurations_WorkspaceId_SortOrder",
|
||||
table: "WorkspaceApprovalStepConfigurations",
|
||||
columns: new[] { "WorkspaceId", "SortOrder" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "WorkspaceApprovalStepConfigurations");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,117 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddApprovalWorkflowRuntime : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "WorkflowInstanceId",
|
||||
table: "ApprovalRequests",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "WorkflowStepRequiredApproverCount",
|
||||
table: "ApprovalRequests",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "WorkflowStepSortOrder",
|
||||
table: "ApprovalRequests",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "WorkflowStepTargetType",
|
||||
table: "ApprovalRequests",
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "WorkflowStepTargetValue",
|
||||
table: "ApprovalRequests",
|
||||
type: "character varying(128)",
|
||||
maxLength: 128,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ApprovalWorkflowInstances",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
WorkspaceId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ContentItemId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
State = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
ApprovalMode = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
StartedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||
CompletedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ApprovalWorkflowInstances", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApprovalRequests_WorkflowInstanceId",
|
||||
table: "ApprovalRequests",
|
||||
column: "WorkflowInstanceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApprovalWorkflowInstances_ContentItemId",
|
||||
table: "ApprovalWorkflowInstances",
|
||||
column: "ContentItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApprovalWorkflowInstances_ContentItemId_State",
|
||||
table: "ApprovalWorkflowInstances",
|
||||
columns: new[] { "ContentItemId", "State" },
|
||||
unique: true,
|
||||
filter: "\"State\" = 'Pending'");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ApprovalWorkflowInstances_WorkspaceId",
|
||||
table: "ApprovalWorkflowInstances",
|
||||
column: "WorkspaceId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ApprovalWorkflowInstances");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ApprovalRequests_WorkflowInstanceId",
|
||||
table: "ApprovalRequests");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WorkflowInstanceId",
|
||||
table: "ApprovalRequests");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WorkflowStepRequiredApproverCount",
|
||||
table: "ApprovalRequests");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WorkflowStepSortOrder",
|
||||
table: "ApprovalRequests");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WorkflowStepTargetType",
|
||||
table: "ApprovalRequests");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WorkflowStepTargetValue",
|
||||
table: "ApprovalRequests");
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,117 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RenameProjectsToCampaigns : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Projects",
|
||||
newName: "Campaigns");
|
||||
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Projects",
|
||||
table: "Campaigns");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Campaigns",
|
||||
table: "Campaigns",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Projects_WorkspaceId",
|
||||
table: "Campaigns",
|
||||
newName: "IX_Campaigns_WorkspaceId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Projects_ClientId_Name",
|
||||
table: "Campaigns",
|
||||
newName: "IX_Campaigns_ClientId_Name");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Projects_ClientId",
|
||||
table: "Campaigns",
|
||||
newName: "IX_Campaigns_ClientId");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "ProjectName",
|
||||
table: "FeedbackReports",
|
||||
newName: "CampaignName");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "ProjectId",
|
||||
table: "FeedbackReports",
|
||||
newName: "CampaignId");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "ProjectId",
|
||||
table: "ContentItems",
|
||||
newName: "CampaignId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_ContentItems_ProjectId",
|
||||
table: "ContentItems",
|
||||
newName: "IX_ContentItems_CampaignId");
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropPrimaryKey(
|
||||
name: "PK_Campaigns",
|
||||
table: "Campaigns");
|
||||
|
||||
migrationBuilder.AddPrimaryKey(
|
||||
name: "PK_Projects",
|
||||
table: "Campaigns",
|
||||
column: "Id");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Campaigns_WorkspaceId",
|
||||
table: "Campaigns",
|
||||
newName: "IX_Projects_WorkspaceId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Campaigns_ClientId_Name",
|
||||
table: "Campaigns",
|
||||
newName: "IX_Projects_ClientId_Name");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Campaigns_ClientId",
|
||||
table: "Campaigns",
|
||||
newName: "IX_Projects_ClientId");
|
||||
|
||||
migrationBuilder.RenameTable(
|
||||
name: "Campaigns",
|
||||
newName: "Projects");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "CampaignName",
|
||||
table: "FeedbackReports",
|
||||
newName: "ProjectName");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "CampaignId",
|
||||
table: "FeedbackReports",
|
||||
newName: "ProjectId");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "CampaignId",
|
||||
table: "ContentItems",
|
||||
newName: "ProjectId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_ContentItems_CampaignId",
|
||||
table: "ContentItems",
|
||||
newName: "IX_ContentItems_ProjectId");
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddOrganizations : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "OrganizationId",
|
||||
table: "Workspaces",
|
||||
type: "uuid",
|
||||
nullable: false,
|
||||
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Organizations",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
OwnerUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Organizations", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OrganizationMemberships",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Role = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OrganizationMemberships", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OrganizationMemberships_Organizations_OrganizationId",
|
||||
column: x => x.OrganizationId,
|
||||
principalTable: "Organizations",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.Sql(
|
||||
"""
|
||||
INSERT INTO "Organizations" ("Id", "Name", "OwnerUserId", "CreatedAt")
|
||||
VALUES ('99999999-9999-9999-9999-999999999999', 'Northstar Collective', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', CURRENT_TIMESTAMP);
|
||||
|
||||
UPDATE "Workspaces"
|
||||
SET "OrganizationId" = '99999999-9999-9999-9999-999999999999'
|
||||
WHERE "OrganizationId" = '00000000-0000-0000-0000-000000000000';
|
||||
|
||||
INSERT INTO "OrganizationMemberships" ("Id", "OrganizationId", "UserId", "Role", "CreatedAt")
|
||||
VALUES ('99999999-9999-9999-9999-000000000001', '99999999-9999-9999-9999-999999999999', 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', 'Owner', CURRENT_TIMESTAMP);
|
||||
""");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Workspaces_OrganizationId",
|
||||
table: "Workspaces",
|
||||
column: "OrganizationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OrganizationMemberships_OrganizationId",
|
||||
table: "OrganizationMemberships",
|
||||
column: "OrganizationId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OrganizationMemberships_OrganizationId_UserId",
|
||||
table: "OrganizationMemberships",
|
||||
columns: new[] { "OrganizationId", "UserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OrganizationMemberships_UserId",
|
||||
table: "OrganizationMemberships",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Organizations_OwnerUserId",
|
||||
table: "Organizations",
|
||||
column: "OwnerUserId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Workspaces_Organizations_OrganizationId",
|
||||
table: "Workspaces",
|
||||
column: "OrganizationId",
|
||||
principalTable: "Organizations",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Workspaces_Organizations_OrganizationId",
|
||||
table: "Workspaces");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OrganizationMemberships");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Organizations");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Workspaces_OrganizationId",
|
||||
table: "Workspaces");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "OrganizationId",
|
||||
table: "Workspaces");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,8 @@ using Socialize.Api.Data;
|
||||
namespace Socialize.Api.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260504195518_AddOrganizations")]
|
||||
partial class AddOrganizations
|
||||
[Migration("20260505204545_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -441,6 +441,326 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("AssetRevisions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarCatalogEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.HasMaxLength(2)
|
||||
.HasColumnType("character varying(2)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("CultureOrReligion")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("DefaultColor")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Region")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("TrustLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Category");
|
||||
|
||||
b.HasIndex("Country");
|
||||
|
||||
b.HasIndex("ProviderName");
|
||||
|
||||
b.ToTable("CalendarCatalogEntries", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = new Guid("10000000-0000-0000-0000-000000000001"),
|
||||
Category = "public-holiday",
|
||||
Country = "US",
|
||||
CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
DefaultColor = "#2F80ED",
|
||||
Description = "Federal public holiday calendar for the United States.",
|
||||
Language = "en",
|
||||
ProviderName = "Nager.Date",
|
||||
SourceUrl = "https://date.nager.at/api/v3/PublicHolidays/2026/US",
|
||||
Title = "United States Public Holidays",
|
||||
TrustLevel = "Verified"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("10000000-0000-0000-0000-000000000002"),
|
||||
Category = "public-holiday",
|
||||
Country = "CA",
|
||||
CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
DefaultColor = "#2F80ED",
|
||||
Description = "Public holiday calendar for Canada.",
|
||||
Language = "en",
|
||||
ProviderName = "Nager.Date",
|
||||
SourceUrl = "https://date.nager.at/api/v3/PublicHolidays/2026/CA",
|
||||
Title = "Canada Public Holidays",
|
||||
TrustLevel = "Verified"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
Category = "marketing-moment",
|
||||
CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
DefaultColor = "#9B51E0",
|
||||
Description = "Common retail, awareness, and social planning moments.",
|
||||
Language = "en",
|
||||
ProviderName = "Socialize",
|
||||
SourceUrl = "https://example.com/socialize/marketing-moments.ics",
|
||||
Title = "Common Marketing Moments",
|
||||
TrustLevel = "Maintained"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("CalendarSourceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateTime?>("EndLocalDateTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("EndUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("ImportedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsAllDay")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsFloatingTime")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("RecurrenceId")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("SourceEventUid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<DateTimeOffset?>("SourceLastModifiedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateTime?>("StartLocalDateTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("TimeZoneId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CalendarSourceId");
|
||||
|
||||
b.HasIndex("CalendarSourceId", "SourceEventUid", "StartDate")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CalendarEvents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarSource", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("CatalogSourceReference")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Color")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("DisplayTitle")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("InheritanceMode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastAttemptedSyncAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastSuccessfulSyncAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("LastSyncError")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<Guid?>("OrganizationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.HasIndex("Scope");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("CalendarSources", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.UserCalendarExportFeed", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<DateTimeOffset?>("RevokedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.HasMaxLength(96)
|
||||
.HasColumnType("character varying(96)");
|
||||
|
||||
b.Property<string>("TokenHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TokenHash")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserCalendarExportFeeds", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -494,6 +814,48 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("Campaigns", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Channels.Data.Channel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("ExternalUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Handle")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Network")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.HasIndex("WorkspaceId", "Network", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Channels", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -550,6 +912,29 @@ namespace Socialize.Api.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.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")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
@@ -576,15 +961,9 @@ namespace Socialize.Api.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid?>("ParentCommentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("ResolvedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
@@ -665,6 +1044,62 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("ContentItems", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemActivityEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ActorEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid?>("ActorUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ContentItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("EntityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("EntityType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("MetadataJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContentItemId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.HasIndex("ContentItemId", "CreatedAt");
|
||||
|
||||
b.ToTable("ContentItemActivityEntries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -1217,6 +1652,10 @@ namespace Socialize.Api.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("LogoUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
@@ -1314,11 +1753,6 @@ namespace Socialize.Api.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("TimeZone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
@@ -1330,9 +1764,6 @@ namespace Socialize.Api.Migrations
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Workspaces", (string)null);
|
||||
});
|
||||
|
||||
@@ -1428,6 +1859,15 @@ namespace Socialize.Api.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarEvent", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarSource", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CalendarSourceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||
1283
backend/src/Socialize.Api/Migrations/20260505204545_Initial.cs
Normal file
1283
backend/src/Socialize.Api/Migrations/20260505204545_Initial.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -438,6 +438,326 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("AssetRevisions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarCatalogEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.HasMaxLength(2)
|
||||
.HasColumnType("character varying(2)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("CultureOrReligion")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("DefaultColor")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<string>("ProviderName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Region")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("TrustLevel")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Category");
|
||||
|
||||
b.HasIndex("Country");
|
||||
|
||||
b.HasIndex("ProviderName");
|
||||
|
||||
b.ToTable("CalendarCatalogEntries", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = new Guid("10000000-0000-0000-0000-000000000001"),
|
||||
Category = "public-holiday",
|
||||
Country = "US",
|
||||
CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
DefaultColor = "#2F80ED",
|
||||
Description = "Federal public holiday calendar for the United States.",
|
||||
Language = "en",
|
||||
ProviderName = "Nager.Date",
|
||||
SourceUrl = "https://date.nager.at/api/v3/PublicHolidays/2026/US",
|
||||
Title = "United States Public Holidays",
|
||||
TrustLevel = "Verified"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("10000000-0000-0000-0000-000000000002"),
|
||||
Category = "public-holiday",
|
||||
Country = "CA",
|
||||
CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
DefaultColor = "#2F80ED",
|
||||
Description = "Public holiday calendar for Canada.",
|
||||
Language = "en",
|
||||
ProviderName = "Nager.Date",
|
||||
SourceUrl = "https://date.nager.at/api/v3/PublicHolidays/2026/CA",
|
||||
Title = "Canada Public Holidays",
|
||||
TrustLevel = "Verified"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = new Guid("10000000-0000-0000-0000-000000000003"),
|
||||
Category = "marketing-moment",
|
||||
CreatedAt = new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
DefaultColor = "#9B51E0",
|
||||
Description = "Common retail, awareness, and social planning moments.",
|
||||
Language = "en",
|
||||
ProviderName = "Socialize",
|
||||
SourceUrl = "https://example.com/socialize/marketing-moments.ics",
|
||||
Title = "Common Marketing Moments",
|
||||
TrustLevel = "Maintained"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("CalendarSourceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("character varying(4000)");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateTime?>("EndLocalDateTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("EndUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("ImportedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsAllDay")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsFloatingTime")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("RecurrenceId")
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<string>("SourceEventUid")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<DateTimeOffset?>("SourceLastModifiedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateTime?>("StartLocalDateTime")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("StartUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("TimeZoneId")
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CalendarSourceId");
|
||||
|
||||
b.HasIndex("CalendarSourceId", "SourceEventUid", "StartDate")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CalendarEvents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarSource", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("CatalogSourceReference")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Category")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("Color")
|
||||
.IsRequired()
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("character varying(16)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("DisplayTitle")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("InheritanceMode")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastAttemptedSyncAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastSuccessfulSyncAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("LastSyncError")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<Guid?>("OrganizationId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Scope")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrganizationId");
|
||||
|
||||
b.HasIndex("Scope");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.ToTable("CalendarSources", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.UserCalendarExportFeed", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<DateTimeOffset?>("RevokedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.HasMaxLength(96)
|
||||
.HasColumnType("character varying(96)");
|
||||
|
||||
b.Property<string>("TokenHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TokenHash")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserCalendarExportFeeds", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -491,6 +811,48 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("Campaigns", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Channels.Data.Channel", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("ExternalUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Handle")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Network")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.HasIndex("WorkspaceId", "Network", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Channels", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -547,6 +909,29 @@ namespace Socialize.Api.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.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")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
@@ -573,15 +958,9 @@ namespace Socialize.Api.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<Guid?>("ParentCommentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("ResolvedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
@@ -662,6 +1041,62 @@ namespace Socialize.Api.Migrations
|
||||
b.ToTable("ContentItems", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemActivityEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ActorEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<Guid?>("ActorUserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ContentItemId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<Guid>("EntityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("EntityType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("MetadataJson")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<Guid>("WorkspaceId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ContentItemId");
|
||||
|
||||
b.HasIndex("WorkspaceId");
|
||||
|
||||
b.HasIndex("ContentItemId", "CreatedAt");
|
||||
|
||||
b.ToTable("ContentItemActivityEntries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -1214,6 +1649,10 @@ namespace Socialize.Api.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("LogoUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
@@ -1311,11 +1750,6 @@ namespace Socialize.Api.Migrations
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<string>("TimeZone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
@@ -1327,9 +1761,6 @@ namespace Socialize.Api.Migrations
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Workspaces", (string)null);
|
||||
});
|
||||
|
||||
@@ -1425,6 +1856,15 @@ namespace Socialize.Api.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarEvent", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Api.Modules.CalendarIntegrations.Data.CalendarSource", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CalendarSourceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b =>
|
||||
{
|
||||
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||
|
||||
@@ -3,10 +3,12 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.Approvals.Data;
|
||||
using Socialize.Api.Modules.Approvals.Services;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||
|
||||
@@ -33,6 +35,7 @@ public class SubmitApprovalDecisionHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<SubmitApprovalDecisionRequest, ApprovalRequestDto>
|
||||
{
|
||||
@@ -120,6 +123,24 @@ public class SubmitApprovalDecisionHandler(
|
||||
dbContext.ApprovalDecisions.Add(decision);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await activityWriter.WriteAsync(
|
||||
new ContentItemActivityWriteModel(
|
||||
approval.WorkspaceId,
|
||||
approval.ContentItemId,
|
||||
"approval.decision.recorded",
|
||||
"ApprovalDecision",
|
||||
decision.Id,
|
||||
$"{decidedByName} recorded {normalizedDecision} for {contentItem.Title}.",
|
||||
decision.DecidedByUserId,
|
||||
decidedByEmail,
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
stage = approval.Stage,
|
||||
status = contentItem.Status,
|
||||
decision = normalizedDecision,
|
||||
})),
|
||||
ct);
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
approval.WorkspaceId,
|
||||
|
||||
@@ -3,8 +3,10 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||
|
||||
@@ -27,6 +29,7 @@ public class CreateAssetRevisionRequestValidator
|
||||
public class CreateAssetRevisionHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<CreateAssetRevisionRequest, AssetRevisionDto>
|
||||
{
|
||||
@@ -78,6 +81,25 @@ public class CreateAssetRevisionHandler(
|
||||
|
||||
if (contentItem is not null)
|
||||
{
|
||||
await activityWriter.WriteAsync(
|
||||
new ContentItemActivityWriteModel(
|
||||
asset.WorkspaceId,
|
||||
asset.ContentItemId,
|
||||
"asset.revision.created",
|
||||
"AssetRevision",
|
||||
revision.Id,
|
||||
$"A new asset revision was added to {asset.DisplayName}.",
|
||||
User.GetUserId(),
|
||||
User.GetEmail(),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
assetId = asset.Id,
|
||||
revisionNumber,
|
||||
sourceReference = revision.SourceReference,
|
||||
notes = revision.Notes,
|
||||
})),
|
||||
ct);
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
asset.WorkspaceId,
|
||||
|
||||
@@ -3,8 +3,10 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Assets.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||
|
||||
@@ -35,6 +37,7 @@ public class CreateGoogleDriveAssetRequestValidator
|
||||
public class CreateGoogleDriveAssetHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<CreateGoogleDriveAssetRequest, AssetDto>
|
||||
{
|
||||
@@ -93,6 +96,25 @@ public class CreateGoogleDriveAssetHandler(
|
||||
dbContext.AssetRevisions.Add(revision);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await activityWriter.WriteAsync(
|
||||
new ContentItemActivityWriteModel(
|
||||
asset.WorkspaceId,
|
||||
asset.ContentItemId,
|
||||
"asset.google-drive-linked",
|
||||
"Asset",
|
||||
asset.Id,
|
||||
$"Google Drive asset {asset.DisplayName} was linked to {contentItem.Title}.",
|
||||
User.GetUserId(),
|
||||
User.GetEmail(),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
assetType = asset.AssetType,
|
||||
sourceType = asset.SourceType,
|
||||
googleDriveFileId = asset.GoogleDriveFileId,
|
||||
currentRevisionNumber = asset.CurrentRevisionNumber,
|
||||
})),
|
||||
ct);
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
asset.WorkspaceId,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
|
||||
public class CalendarCatalogEntry
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public required string Title { get; set; }
|
||||
public required string Description { get; set; }
|
||||
public string? Country { get; set; }
|
||||
public string? Region { get; set; }
|
||||
public required string Language { get; set; }
|
||||
public required string Category { get; set; }
|
||||
public string? CultureOrReligion { get; set; }
|
||||
public required string ProviderName { get; set; }
|
||||
public required string SourceUrl { get; set; }
|
||||
public required string TrustLevel { get; set; }
|
||||
public required string DefaultColor { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
|
||||
public static class CalendarCatalogSeed
|
||||
{
|
||||
public static readonly CalendarCatalogEntry[] Entries =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("10000000-0000-0000-0000-000000000001"),
|
||||
Title = "United States Public Holidays",
|
||||
Description = "Federal public holiday calendar for the United States.",
|
||||
Country = "US",
|
||||
Region = null,
|
||||
Language = "en",
|
||||
Category = "public-holiday",
|
||||
CultureOrReligion = null,
|
||||
ProviderName = "Nager.Date",
|
||||
SourceUrl = "https://date.nager.at/api/v3/PublicHolidays/2026/US",
|
||||
TrustLevel = "Verified",
|
||||
DefaultColor = "#2F80ED",
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("10000000-0000-0000-0000-000000000002"),
|
||||
Title = "Canada Public Holidays",
|
||||
Description = "Public holiday calendar for Canada.",
|
||||
Country = "CA",
|
||||
Region = null,
|
||||
Language = "en",
|
||||
Category = "public-holiday",
|
||||
CultureOrReligion = null,
|
||||
ProviderName = "Nager.Date",
|
||||
SourceUrl = "https://date.nager.at/api/v3/PublicHolidays/2026/CA",
|
||||
TrustLevel = "Verified",
|
||||
DefaultColor = "#2F80ED",
|
||||
},
|
||||
new()
|
||||
{
|
||||
Id = Guid.Parse("10000000-0000-0000-0000-000000000003"),
|
||||
Title = "Common Marketing Moments",
|
||||
Description = "Common retail, awareness, and social planning moments.",
|
||||
Country = null,
|
||||
Region = null,
|
||||
Language = "en",
|
||||
Category = "marketing-moment",
|
||||
CultureOrReligion = null,
|
||||
ProviderName = "Socialize",
|
||||
SourceUrl = "https://example.com/socialize/marketing-moments.ics",
|
||||
TrustLevel = "Maintained",
|
||||
DefaultColor = "#9B51E0",
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
|
||||
public class CalendarEvent
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid CalendarSourceId { get; set; }
|
||||
public required string SourceEventUid { get; set; }
|
||||
public required string Title { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool IsAllDay { get; set; }
|
||||
public bool IsFloatingTime { get; set; }
|
||||
public DateOnly StartDate { get; set; }
|
||||
public DateOnly EndDate { get; set; }
|
||||
public DateTime? StartLocalDateTime { get; set; }
|
||||
public DateTime? EndLocalDateTime { get; set; }
|
||||
public DateTimeOffset? StartUtc { get; set; }
|
||||
public DateTimeOffset? EndUtc { get; set; }
|
||||
public string? TimeZoneId { get; set; }
|
||||
public string? RecurrenceId { get; set; }
|
||||
public string? Location { get; set; }
|
||||
public string? SourceUrl { get; set; }
|
||||
public DateTimeOffset? SourceLastModifiedAt { get; set; }
|
||||
public DateTimeOffset ImportedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
|
||||
public class CalendarSource
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public required string Scope { get; set; }
|
||||
public Guid? OrganizationId { get; set; }
|
||||
public Guid? WorkspaceId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
public string? SourceUrl { get; set; }
|
||||
public string? CatalogSourceReference { get; set; }
|
||||
public required string DisplayTitle { get; set; }
|
||||
public required string Color { get; set; }
|
||||
public required string Category { get; set; }
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public string? InheritanceMode { get; set; }
|
||||
public DateTimeOffset? LastSuccessfulSyncAt { get; set; }
|
||||
public DateTimeOffset? LastAttemptedSyncAt { get; set; }
|
||||
public string? LastSyncError { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
|
||||
public static class CalendarSourceModelConfiguration
|
||||
{
|
||||
public static ModelBuilder ConfigureCalendarIntegrationsModule(this ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<CalendarSource>(source =>
|
||||
{
|
||||
source.ToTable("CalendarSources");
|
||||
source.HasKey(x => x.Id);
|
||||
source.Property(x => x.Scope).HasMaxLength(32).IsRequired();
|
||||
source.Property(x => x.SourceUrl).HasMaxLength(2048);
|
||||
source.Property(x => x.CatalogSourceReference).HasMaxLength(256);
|
||||
source.Property(x => x.DisplayTitle).HasMaxLength(256).IsRequired();
|
||||
source.Property(x => x.Color).HasMaxLength(16).IsRequired();
|
||||
source.Property(x => x.Category).HasMaxLength(64).IsRequired();
|
||||
source.Property(x => x.InheritanceMode).HasMaxLength(32);
|
||||
source.Property(x => x.LastSyncError).HasMaxLength(2048);
|
||||
source.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
source.Property(x => x.UpdatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
source.HasIndex(x => x.Scope);
|
||||
source.HasIndex(x => x.OrganizationId);
|
||||
source.HasIndex(x => x.WorkspaceId);
|
||||
source.HasIndex(x => x.UserId);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<CalendarCatalogEntry>(entry =>
|
||||
{
|
||||
entry.ToTable("CalendarCatalogEntries");
|
||||
entry.HasKey(x => x.Id);
|
||||
entry.Property(x => x.Title).HasMaxLength(256).IsRequired();
|
||||
entry.Property(x => x.Description).HasMaxLength(1024).IsRequired();
|
||||
entry.Property(x => x.Country).HasMaxLength(2);
|
||||
entry.Property(x => x.Region).HasMaxLength(128);
|
||||
entry.Property(x => x.Language).HasMaxLength(16).IsRequired();
|
||||
entry.Property(x => x.Category).HasMaxLength(64).IsRequired();
|
||||
entry.Property(x => x.CultureOrReligion).HasMaxLength(128);
|
||||
entry.Property(x => x.ProviderName).HasMaxLength(128).IsRequired();
|
||||
entry.Property(x => x.SourceUrl).HasMaxLength(2048).IsRequired();
|
||||
entry.Property(x => x.TrustLevel).HasMaxLength(64).IsRequired();
|
||||
entry.Property(x => x.DefaultColor).HasMaxLength(16).IsRequired();
|
||||
entry.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
entry.HasIndex(x => x.Country);
|
||||
entry.HasIndex(x => x.Category);
|
||||
entry.HasIndex(x => x.ProviderName);
|
||||
entry.HasData(CalendarCatalogSeed.Entries);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<CalendarEvent>(calendarEvent =>
|
||||
{
|
||||
calendarEvent.ToTable("CalendarEvents");
|
||||
calendarEvent.HasKey(x => x.Id);
|
||||
calendarEvent.Property(x => x.SourceEventUid).HasMaxLength(512).IsRequired();
|
||||
calendarEvent.Property(x => x.Title).HasMaxLength(512).IsRequired();
|
||||
calendarEvent.Property(x => x.Description).HasMaxLength(4000);
|
||||
calendarEvent.Property(x => x.TimeZoneId).HasMaxLength(128);
|
||||
calendarEvent.Property(x => x.RecurrenceId).HasMaxLength(512);
|
||||
calendarEvent.Property(x => x.Location).HasMaxLength(512);
|
||||
calendarEvent.Property(x => x.SourceUrl).HasMaxLength(2048);
|
||||
calendarEvent.HasIndex(x => x.CalendarSourceId);
|
||||
calendarEvent.HasIndex(x => new { x.CalendarSourceId, x.SourceEventUid, x.StartDate }).IsUnique();
|
||||
calendarEvent.HasOne<CalendarSource>()
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CalendarSourceId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<UserCalendarExportFeed>(feed =>
|
||||
{
|
||||
feed.ToTable("UserCalendarExportFeeds");
|
||||
feed.HasKey(x => x.Id);
|
||||
feed.Property(x => x.Token).HasMaxLength(96);
|
||||
feed.Property(x => x.TokenHash).HasMaxLength(64);
|
||||
feed.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
feed.Property(x => x.UpdatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
feed.HasIndex(x => x.UserId).IsUnique();
|
||||
feed.HasIndex(x => x.TokenHash).IsUnique();
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
|
||||
public class UserCalendarExportFeed
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid UserId { get; set; }
|
||||
public string? Token { get; set; }
|
||||
public string? TokenHash { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
public DateTimeOffset? RevokedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
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.CalendarImportSyncService>();
|
||||
builder.Services.AddHostedService<Services.CalendarImportBackgroundService>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||
|
||||
public record CalendarSourceDto(
|
||||
Guid Id,
|
||||
string Scope,
|
||||
Guid? OrganizationId,
|
||||
Guid? WorkspaceId,
|
||||
Guid? UserId,
|
||||
string? SourceUrl,
|
||||
string? CatalogSourceReference,
|
||||
string DisplayTitle,
|
||||
string Color,
|
||||
string Category,
|
||||
bool IsEnabled,
|
||||
string? InheritanceMode,
|
||||
bool IsReadOnly,
|
||||
DateTimeOffset? LastSuccessfulSyncAt,
|
||||
DateTimeOffset? LastAttemptedSyncAt,
|
||||
string? LastSyncError,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset UpdatedAt)
|
||||
{
|
||||
public static CalendarSourceDto FromSource(CalendarSource source, bool isReadOnly)
|
||||
{
|
||||
return new CalendarSourceDto(
|
||||
source.Id,
|
||||
source.Scope,
|
||||
source.OrganizationId,
|
||||
source.WorkspaceId,
|
||||
source.UserId,
|
||||
source.SourceUrl,
|
||||
source.CatalogSourceReference,
|
||||
source.DisplayTitle,
|
||||
source.Color,
|
||||
source.Category,
|
||||
source.IsEnabled,
|
||||
source.InheritanceMode,
|
||||
isReadOnly,
|
||||
source.LastSuccessfulSyncAt,
|
||||
source.LastAttemptedSyncAt,
|
||||
source.LastSyncError,
|
||||
source.CreatedAt,
|
||||
source.UpdatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
public record UpsertCalendarSourceRequest(
|
||||
string Scope,
|
||||
Guid? OrganizationId,
|
||||
Guid? WorkspaceId,
|
||||
string? SourceUrl,
|
||||
string? CatalogSourceReference,
|
||||
string DisplayTitle,
|
||||
string Color,
|
||||
string Category,
|
||||
bool IsEnabled,
|
||||
string? InheritanceMode);
|
||||
|
||||
public class UpsertCalendarSourceRequestValidator
|
||||
: FastEndpoints.Validator<UpsertCalendarSourceRequest>
|
||||
{
|
||||
public UpsertCalendarSourceRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Scope)
|
||||
.NotEmpty()
|
||||
.Must(CalendarSourceRules.IsSupportedScope)
|
||||
.WithMessage("A valid calendar source scope should be specified.");
|
||||
|
||||
RuleFor(x => x.DisplayTitle).NotEmpty().MaximumLength(256);
|
||||
RuleFor(x => x.Category).NotEmpty().MaximumLength(64);
|
||||
RuleFor(x => x.Color)
|
||||
.NotEmpty()
|
||||
.Matches("^#[0-9A-Fa-f]{6}$")
|
||||
.WithMessage("Color should be a six digit hex color, for example #2F80ED.");
|
||||
|
||||
RuleFor(x => x.SourceUrl)
|
||||
.MaximumLength(2048)
|
||||
.Must(value => string.IsNullOrWhiteSpace(value) || Uri.TryCreate(value, UriKind.Absolute, out Uri? uri) &&
|
||||
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
|
||||
.WithMessage("Source URL should be an absolute HTTP or HTTPS URL.");
|
||||
|
||||
RuleFor(x => x.CatalogSourceReference).MaximumLength(256);
|
||||
|
||||
RuleFor(x => x)
|
||||
.Must(x => !string.IsNullOrWhiteSpace(x.SourceUrl) || !string.IsNullOrWhiteSpace(x.CatalogSourceReference))
|
||||
.WithMessage("A source URL or catalog source reference should be specified.");
|
||||
|
||||
RuleFor(x => x)
|
||||
.Must(x => x.Scope != CalendarSourceScopes.Organization || x.OrganizationId.HasValue)
|
||||
.WithMessage("Organization calendar sources require an organization id.");
|
||||
|
||||
RuleFor(x => x)
|
||||
.Must(x => x.Scope != CalendarSourceScopes.Workspace || x.WorkspaceId.HasValue)
|
||||
.WithMessage("Workspace calendar sources require a workspace id.");
|
||||
|
||||
RuleFor(x => x)
|
||||
.Must(x => x.Scope != CalendarSourceScopes.User || (!x.OrganizationId.HasValue && !x.WorkspaceId.HasValue))
|
||||
.WithMessage("User calendar sources should not include organization or workspace ids.");
|
||||
|
||||
RuleFor(x => x)
|
||||
.Must(x => x.Scope == CalendarSourceScopes.Organization ||
|
||||
(!x.OrganizationId.HasValue && string.IsNullOrWhiteSpace(x.InheritanceMode)))
|
||||
.WithMessage("Only organization calendar sources can set organization ids or inheritance modes.");
|
||||
|
||||
RuleFor(x => x.InheritanceMode)
|
||||
.Must(value => string.IsNullOrWhiteSpace(value) || CalendarSourceRules.IsSupportedInheritanceMode(value))
|
||||
.WithMessage("A valid inheritance mode should be specified.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
using Socialize.Api.Modules.Organizations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||
|
||||
public class CreateCalendarSourceHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
OrganizationAccessService organizationAccessService)
|
||||
: Endpoint<UpsertCalendarSourceRequest, CalendarSourceDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/calendar-integrations/sources");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UpsertCalendarSourceRequest request, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Guid currentUserId = User.GetUserId();
|
||||
string scope = request.Scope.Trim();
|
||||
Guid? organizationId = request.OrganizationId;
|
||||
Guid? workspaceId = request.WorkspaceId;
|
||||
|
||||
if (!await CanCreateAsync(scope, organizationId, workspaceId, currentUserId, ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
string? sourceUrl = NormalizeOptional(request.SourceUrl);
|
||||
string? catalogSourceReference = NormalizeOptional(request.CatalogSourceReference);
|
||||
if (await SourceAlreadyExistsAsync(scope, organizationId, workspaceId, currentUserId, sourceUrl, catalogSourceReference, ct))
|
||||
{
|
||||
AddError(request => request.SourceUrl, "This calendar source has already been added.");
|
||||
await SendErrorsAsync(cancellation: ct);
|
||||
return;
|
||||
}
|
||||
|
||||
CalendarSource source = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Scope = scope,
|
||||
OrganizationId = scope == CalendarSourceScopes.Organization ? organizationId : null,
|
||||
WorkspaceId = scope == CalendarSourceScopes.Workspace ? workspaceId : null,
|
||||
UserId = scope == CalendarSourceScopes.User ? currentUserId : null,
|
||||
SourceUrl = sourceUrl,
|
||||
CatalogSourceReference = catalogSourceReference,
|
||||
DisplayTitle = request.DisplayTitle.Trim(),
|
||||
Color = request.Color.Trim(),
|
||||
Category = request.Category.Trim(),
|
||||
IsEnabled = request.IsEnabled,
|
||||
InheritanceMode = scope == CalendarSourceScopes.Organization
|
||||
? NormalizeOptional(request.InheritanceMode) ?? CalendarSourceInheritanceModes.Optional
|
||||
: null,
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
dbContext.CalendarSources.Add(source);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendAsync(CalendarSourceDto.FromSource(source, isReadOnly: false), StatusCodes.Status201Created, ct);
|
||||
}
|
||||
|
||||
private async Task<bool> CanCreateAsync(
|
||||
string scope,
|
||||
Guid? organizationId,
|
||||
Guid? workspaceId,
|
||||
Guid currentUserId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return scope switch
|
||||
{
|
||||
CalendarSourceScopes.Organization when organizationId.HasValue =>
|
||||
await dbContext.Organizations.AnyAsync(organization => organization.Id == organizationId.Value, ct) &&
|
||||
await organizationAccessService.HasOrganizationPermissionAsync(
|
||||
User,
|
||||
organizationId.Value,
|
||||
OrganizationPermissions.ManageConnectors,
|
||||
ct),
|
||||
CalendarSourceScopes.Workspace when workspaceId.HasValue =>
|
||||
await dbContext.Workspaces.AnyAsync(workspace => workspace.Id == workspaceId.Value, ct) &&
|
||||
await accessScopeService.CanManageWorkspaceAsync(User, workspaceId.Value, ct),
|
||||
CalendarSourceScopes.User => currentUserId != Guid.Empty,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private Task<bool> SourceAlreadyExistsAsync(
|
||||
string scope,
|
||||
Guid? organizationId,
|
||||
Guid? workspaceId,
|
||||
Guid currentUserId,
|
||||
string? sourceUrl,
|
||||
string? catalogSourceReference,
|
||||
CancellationToken ct)
|
||||
{
|
||||
IQueryable<CalendarSource> query = dbContext.CalendarSources
|
||||
.Where(source => source.Scope == scope);
|
||||
|
||||
query = scope switch
|
||||
{
|
||||
CalendarSourceScopes.Organization => query.Where(source => source.OrganizationId == organizationId),
|
||||
CalendarSourceScopes.Workspace => query.Where(source => source.WorkspaceId == workspaceId),
|
||||
CalendarSourceScopes.User => query.Where(source => source.UserId == currentUserId),
|
||||
_ => query.Where(_ => false),
|
||||
};
|
||||
|
||||
string? normalizedUrl = sourceUrl?.Trim();
|
||||
string? normalizedCatalogReference = catalogSourceReference?.Trim();
|
||||
|
||||
return query.AnyAsync(source =>
|
||||
(!string.IsNullOrWhiteSpace(normalizedCatalogReference) &&
|
||||
source.CatalogSourceReference == normalizedCatalogReference) ||
|
||||
(!string.IsNullOrWhiteSpace(normalizedUrl) &&
|
||||
source.SourceUrl != null &&
|
||||
source.SourceUrl.ToUpper() == normalizedUrl.ToUpper()),
|
||||
ct);
|
||||
}
|
||||
|
||||
private static string? NormalizeOptional(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
using Socialize.Api.Modules.Organizations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||
|
||||
public class DeleteCalendarSourceHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
OrganizationAccessService organizationAccessService)
|
||||
: EndpointWithoutRequest
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/api/calendar-integrations/sources/{sourceId:guid}");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
Guid sourceId = Route<Guid>("sourceId");
|
||||
CalendarSource? source = await dbContext.CalendarSources.SingleOrDefaultAsync(candidate => candidate.Id == sourceId, ct);
|
||||
if (source is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await CanManageExistingSourceAsync(source, User.GetUserId(), ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
dbContext.CalendarSources.Remove(source);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendNoContentAsync(ct);
|
||||
}
|
||||
|
||||
private async Task<bool> CanManageExistingSourceAsync(
|
||||
CalendarSource source,
|
||||
Guid currentUserId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return source.Scope switch
|
||||
{
|
||||
CalendarSourceScopes.Organization when source.OrganizationId.HasValue =>
|
||||
await organizationAccessService.HasOrganizationPermissionAsync(
|
||||
User,
|
||||
source.OrganizationId.Value,
|
||||
OrganizationPermissions.ManageConnectors,
|
||||
ct),
|
||||
CalendarSourceScopes.Workspace when source.WorkspaceId.HasValue =>
|
||||
await accessScopeService.CanManageWorkspaceAsync(User, source.WorkspaceId.Value, ct),
|
||||
CalendarSourceScopes.User => source.UserId == currentUserId,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||
|
||||
public sealed class ListCalendarCatalogRequest
|
||||
{
|
||||
public string? Search { get; set; }
|
||||
public string? Country { get; set; }
|
||||
public string? Region { get; set; }
|
||||
public string? Language { get; set; }
|
||||
public string? Category { get; set; }
|
||||
public string? CultureOrReligion { get; set; }
|
||||
public string? Provider { get; set; }
|
||||
}
|
||||
|
||||
public record CalendarCatalogEntryDto(
|
||||
Guid Id,
|
||||
string Title,
|
||||
string Description,
|
||||
string? Country,
|
||||
string? Region,
|
||||
string Language,
|
||||
string Category,
|
||||
string? CultureOrReligion,
|
||||
string ProviderName,
|
||||
string SourceUrl,
|
||||
string TrustLevel,
|
||||
string DefaultColor);
|
||||
|
||||
public class ListCalendarCatalogHandler(AppDbContext dbContext)
|
||||
: Endpoint<ListCalendarCatalogRequest, IReadOnlyCollection<CalendarCatalogEntryDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/calendar-integrations/catalog");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ListCalendarCatalogRequest request, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
IQueryable<CalendarCatalogEntry> query = dbContext.CalendarCatalogEntries.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Search))
|
||||
{
|
||||
string search = request.Search.Trim().ToLowerInvariant();
|
||||
query = query.Where(entry =>
|
||||
entry.Title.ToLower().Contains(search) ||
|
||||
entry.Description.ToLower().Contains(search) ||
|
||||
entry.ProviderName.ToLower().Contains(search));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Country))
|
||||
{
|
||||
string country = request.Country.Trim().ToUpperInvariant();
|
||||
query = query.Where(entry => entry.Country == country);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Region))
|
||||
{
|
||||
string region = request.Region.Trim();
|
||||
query = query.Where(entry => entry.Region == region);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Language))
|
||||
{
|
||||
string language = request.Language.Trim();
|
||||
query = query.Where(entry => entry.Language == language);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Category))
|
||||
{
|
||||
string category = request.Category.Trim();
|
||||
query = query.Where(entry => entry.Category == category);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.CultureOrReligion))
|
||||
{
|
||||
string cultureOrReligion = request.CultureOrReligion.Trim();
|
||||
query = query.Where(entry => entry.CultureOrReligion == cultureOrReligion);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Provider))
|
||||
{
|
||||
string provider = request.Provider.Trim();
|
||||
query = query.Where(entry => entry.ProviderName == provider);
|
||||
}
|
||||
|
||||
CalendarCatalogEntryDto[] entries = await query
|
||||
.OrderBy(entry => entry.Country)
|
||||
.ThenBy(entry => entry.Category)
|
||||
.ThenBy(entry => entry.Title)
|
||||
.Take(100)
|
||||
.Select(entry => new CalendarCatalogEntryDto(
|
||||
entry.Id,
|
||||
entry.Title,
|
||||
entry.Description,
|
||||
entry.Country,
|
||||
entry.Region,
|
||||
entry.Language,
|
||||
entry.Category,
|
||||
entry.CultureOrReligion,
|
||||
entry.ProviderName,
|
||||
entry.SourceUrl,
|
||||
entry.TrustLevel,
|
||||
entry.DefaultColor))
|
||||
.ToArrayAsync(ct);
|
||||
|
||||
await SendOkAsync(entries, ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||
|
||||
public sealed class ListCalendarEventsRequest
|
||||
{
|
||||
public Guid? WorkspaceId { get; set; }
|
||||
public DateOnly? StartDate { get; set; }
|
||||
public DateOnly? EndDate { get; set; }
|
||||
}
|
||||
|
||||
public record CalendarEventDto(
|
||||
Guid Id,
|
||||
Guid CalendarSourceId,
|
||||
string SourceEventUid,
|
||||
string Title,
|
||||
string? Description,
|
||||
bool IsAllDay,
|
||||
bool IsFloatingTime,
|
||||
DateOnly StartDate,
|
||||
DateOnly EndDate,
|
||||
DateTime? StartLocalDateTime,
|
||||
DateTime? EndLocalDateTime,
|
||||
DateTimeOffset? StartUtc,
|
||||
DateTimeOffset? EndUtc,
|
||||
string? TimeZoneId,
|
||||
string? RecurrenceId,
|
||||
string? Location,
|
||||
string? SourceUrl,
|
||||
DateTimeOffset? SourceLastModifiedAt,
|
||||
DateTimeOffset ImportedAt);
|
||||
|
||||
public class ListCalendarEventsHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService)
|
||||
: Endpoint<ListCalendarEventsRequest, IReadOnlyCollection<CalendarEventDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/calendar-integrations/events");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ListCalendarEventsRequest request, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Guid currentUserId = User.GetUserId();
|
||||
DateOnly startDate = request.StartDate ?? DateOnly.FromDateTime(DateTime.UtcNow.Date.AddMonths(-1));
|
||||
DateOnly endDate = request.EndDate ?? DateOnly.FromDateTime(DateTime.UtcNow.Date.AddMonths(3));
|
||||
|
||||
if (request.WorkspaceId.HasValue &&
|
||||
!await accessScopeService.CanAccessWorkspaceAsync(User, request.WorkspaceId.Value, ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
IQueryable<CalendarSource> visibleSources = dbContext.CalendarSources
|
||||
.Where(source => source.IsEnabled);
|
||||
|
||||
if (request.WorkspaceId.HasValue)
|
||||
{
|
||||
Guid? organizationId = await dbContext.Workspaces
|
||||
.Where(workspace => workspace.Id == request.WorkspaceId.Value)
|
||||
.Select(workspace => (Guid?)workspace.OrganizationId)
|
||||
.SingleOrDefaultAsync(ct);
|
||||
|
||||
if (!organizationId.HasValue)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
visibleSources = visibleSources.Where(source =>
|
||||
source.Scope == CalendarSourceScopes.Organization && source.OrganizationId == organizationId ||
|
||||
source.Scope == CalendarSourceScopes.Workspace && source.WorkspaceId == request.WorkspaceId ||
|
||||
source.Scope == CalendarSourceScopes.User && source.UserId == currentUserId);
|
||||
}
|
||||
else
|
||||
{
|
||||
IReadOnlyCollection<Guid> workspaceIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||
Guid[] organizationIds = await dbContext.Workspaces
|
||||
.Where(workspace => workspaceIds.Contains(workspace.Id))
|
||||
.Select(workspace => workspace.OrganizationId)
|
||||
.Distinct()
|
||||
.ToArrayAsync(ct);
|
||||
|
||||
visibleSources = visibleSources.Where(source =>
|
||||
source.Scope == CalendarSourceScopes.Organization && source.OrganizationId.HasValue && organizationIds.Contains(source.OrganizationId.Value) ||
|
||||
source.Scope == CalendarSourceScopes.Workspace && source.WorkspaceId.HasValue && workspaceIds.Contains(source.WorkspaceId.Value) ||
|
||||
source.Scope == CalendarSourceScopes.User && source.UserId == currentUserId);
|
||||
}
|
||||
|
||||
Guid[] sourceIds = await visibleSources
|
||||
.Select(source => source.Id)
|
||||
.ToArrayAsync(ct);
|
||||
|
||||
CalendarEventDto[] events = await dbContext.CalendarEvents
|
||||
.Where(calendarEvent => sourceIds.Contains(calendarEvent.CalendarSourceId))
|
||||
.Where(calendarEvent => calendarEvent.StartDate <= endDate && calendarEvent.EndDate >= startDate)
|
||||
.OrderBy(calendarEvent => calendarEvent.StartDate)
|
||||
.ThenBy(calendarEvent => calendarEvent.Title)
|
||||
.Select(calendarEvent => new CalendarEventDto(
|
||||
calendarEvent.Id,
|
||||
calendarEvent.CalendarSourceId,
|
||||
calendarEvent.SourceEventUid,
|
||||
calendarEvent.Title,
|
||||
calendarEvent.Description,
|
||||
calendarEvent.IsAllDay,
|
||||
calendarEvent.IsFloatingTime,
|
||||
calendarEvent.StartDate,
|
||||
calendarEvent.EndDate,
|
||||
calendarEvent.StartLocalDateTime,
|
||||
calendarEvent.EndLocalDateTime,
|
||||
calendarEvent.StartUtc,
|
||||
calendarEvent.EndUtc,
|
||||
calendarEvent.TimeZoneId,
|
||||
calendarEvent.RecurrenceId,
|
||||
calendarEvent.Location,
|
||||
calendarEvent.SourceUrl,
|
||||
calendarEvent.SourceLastModifiedAt,
|
||||
calendarEvent.ImportedAt))
|
||||
.ToArrayAsync(ct);
|
||||
|
||||
await SendOkAsync(events, ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||
|
||||
public record ListCalendarSourcesRequest(Guid? WorkspaceId);
|
||||
|
||||
public class ListCalendarSourcesHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService)
|
||||
: Endpoint<ListCalendarSourcesRequest, IReadOnlyCollection<CalendarSourceDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/calendar-integrations/sources");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ListCalendarSourcesRequest request, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Guid currentUserId = User.GetUserId();
|
||||
List<CalendarSource> sources;
|
||||
|
||||
if (request.WorkspaceId.HasValue)
|
||||
{
|
||||
var workspace = await dbContext.Workspaces
|
||||
.Where(candidate => candidate.Id == request.WorkspaceId.Value)
|
||||
.Select(candidate => new { candidate.Id, candidate.OrganizationId })
|
||||
.SingleOrDefaultAsync(ct);
|
||||
|
||||
if (workspace is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await accessScopeService.CanAccessWorkspaceAsync(User, workspace.Id, ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
sources = await dbContext.CalendarSources
|
||||
.Where(source =>
|
||||
source.Scope == CalendarSourceScopes.Organization && source.OrganizationId == workspace.OrganizationId ||
|
||||
source.Scope == CalendarSourceScopes.Workspace && source.WorkspaceId == workspace.Id ||
|
||||
source.Scope == CalendarSourceScopes.User && source.UserId == currentUserId)
|
||||
.OrderBy(source => source.Scope)
|
||||
.ThenBy(source => source.DisplayTitle)
|
||||
.ToListAsync(ct);
|
||||
|
||||
await SendOkAsync(
|
||||
sources
|
||||
.Select(source => CalendarSourceDto.FromSource(
|
||||
source,
|
||||
CalendarSourceRules.IsInheritedOrganizationSource(source, workspace.OrganizationId)))
|
||||
.ToArray(),
|
||||
ct);
|
||||
return;
|
||||
}
|
||||
|
||||
sources = await dbContext.CalendarSources
|
||||
.Where(source => source.Scope == CalendarSourceScopes.User && source.UserId == currentUserId)
|
||||
.OrderBy(source => source.DisplayTitle)
|
||||
.ToListAsync(ct);
|
||||
|
||||
await SendOkAsync(
|
||||
sources.Select(source => CalendarSourceDto.FromSource(source, isReadOnly: false)).ToArray(),
|
||||
ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
using Socialize.Api.Modules.Organizations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||
|
||||
public class RefreshCalendarSourceHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
OrganizationAccessService organizationAccessService,
|
||||
CalendarImportSyncService syncService)
|
||||
: EndpointWithoutRequest<CalendarSourceDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/calendar-integrations/sources/{sourceId:guid}/refresh");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
Guid sourceId = Route<Guid>("sourceId");
|
||||
CalendarSource? source = await dbContext.CalendarSources.SingleOrDefaultAsync(candidate => candidate.Id == sourceId, ct);
|
||||
if (source is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await CanManageExistingSourceAsync(source, User.GetUserId(), ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
await syncService.RefreshSourceAsync(source.Id, ct);
|
||||
await dbContext.Entry(source).ReloadAsync(ct);
|
||||
|
||||
await SendOkAsync(CalendarSourceDto.FromSource(source, isReadOnly: false), ct);
|
||||
}
|
||||
|
||||
private async Task<bool> CanManageExistingSourceAsync(
|
||||
CalendarSource source,
|
||||
Guid currentUserId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return source.Scope switch
|
||||
{
|
||||
CalendarSourceScopes.Organization when source.OrganizationId.HasValue =>
|
||||
await organizationAccessService.HasOrganizationPermissionAsync(
|
||||
User,
|
||||
source.OrganizationId.Value,
|
||||
OrganizationPermissions.ManageConnectors,
|
||||
ct),
|
||||
CalendarSourceScopes.Workspace when source.WorkspaceId.HasValue =>
|
||||
await accessScopeService.CanManageWorkspaceAsync(User, source.WorkspaceId.Value, ct),
|
||||
CalendarSourceScopes.User => source.UserId == currentUserId,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
using Socialize.Api.Modules.Organizations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||
|
||||
public class UpdateCalendarSourceHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
OrganizationAccessService organizationAccessService)
|
||||
: Endpoint<UpsertCalendarSourceRequest, CalendarSourceDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/api/calendar-integrations/sources/{sourceId:guid}");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UpsertCalendarSourceRequest request, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Guid sourceId = Route<Guid>("sourceId");
|
||||
CalendarSource? source = await dbContext.CalendarSources.SingleOrDefaultAsync(candidate => candidate.Id == sourceId, ct);
|
||||
if (source is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
Guid currentUserId = User.GetUserId();
|
||||
if (!await CanManageExistingSourceAsync(source, currentUserId, ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (source.Scope != request.Scope.Trim() ||
|
||||
source.OrganizationId != (request.Scope == CalendarSourceScopes.Organization ? request.OrganizationId : null) ||
|
||||
source.WorkspaceId != (request.Scope == CalendarSourceScopes.Workspace ? request.WorkspaceId : null))
|
||||
{
|
||||
AddError("Calendar source scope cannot be changed.");
|
||||
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
source.SourceUrl = NormalizeOptional(request.SourceUrl);
|
||||
source.CatalogSourceReference = NormalizeOptional(request.CatalogSourceReference);
|
||||
source.DisplayTitle = request.DisplayTitle.Trim();
|
||||
source.Color = request.Color.Trim();
|
||||
source.Category = request.Category.Trim();
|
||||
source.IsEnabled = request.IsEnabled;
|
||||
source.InheritanceMode = source.Scope == CalendarSourceScopes.Organization
|
||||
? NormalizeOptional(request.InheritanceMode) ?? CalendarSourceInheritanceModes.Optional
|
||||
: null;
|
||||
source.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(CalendarSourceDto.FromSource(source, isReadOnly: false), ct);
|
||||
}
|
||||
|
||||
private async Task<bool> CanManageExistingSourceAsync(
|
||||
CalendarSource source,
|
||||
Guid currentUserId,
|
||||
CancellationToken ct)
|
||||
{
|
||||
return source.Scope switch
|
||||
{
|
||||
CalendarSourceScopes.Organization when source.OrganizationId.HasValue =>
|
||||
await organizationAccessService.HasOrganizationPermissionAsync(
|
||||
User,
|
||||
source.OrganizationId.Value,
|
||||
OrganizationPermissions.ManageConnectors,
|
||||
ct),
|
||||
CalendarSourceScopes.Workspace when source.WorkspaceId.HasValue =>
|
||||
await accessScopeService.CanManageWorkspaceAsync(User, source.WorkspaceId.Value, ct),
|
||||
CalendarSourceScopes.User => source.UserId == currentUserId,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static string? NormalizeOptional(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
using Socialize.Api.Modules.Identity.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||
|
||||
public record UserCalendarExportFeedDto(
|
||||
bool IsEnabled,
|
||||
string? FeedUrl,
|
||||
DateTimeOffset? CreatedAt,
|
||||
DateTimeOffset? UpdatedAt,
|
||||
DateTimeOffset? RevokedAt);
|
||||
|
||||
public class GetUserCalendarExportFeedHandler(AppDbContext dbContext)
|
||||
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/calendar-integrations/export-feed");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
UserCalendarExportFeed? feed = await dbContext.UserCalendarExportFeeds
|
||||
.SingleOrDefaultAsync(candidate => candidate.UserId == User.GetUserId(), ct);
|
||||
|
||||
await SendAsync(UserCalendarExportFeedMapper.ToDto(feed, UserCalendarExportFeedMapper.BuildFeedUrl(feed)), cancellation: ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class EnableUserCalendarExportFeedHandler(AppDbContext dbContext)
|
||||
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/calendar-integrations/export-feed/enable");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
Guid userId = User.GetUserId();
|
||||
string token = CalendarExportFeedTokenService.GenerateToken();
|
||||
string tokenHash = CalendarExportFeedTokenService.HashToken(token);
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
|
||||
UserCalendarExportFeed? feed = await dbContext.UserCalendarExportFeeds
|
||||
.SingleOrDefaultAsync(candidate => candidate.UserId == userId, ct);
|
||||
|
||||
if (feed is null)
|
||||
{
|
||||
feed = new UserCalendarExportFeed
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Token = token,
|
||||
TokenHash = tokenHash,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
dbContext.UserCalendarExportFeeds.Add(feed);
|
||||
}
|
||||
else if (feed.TokenHash is null || feed.RevokedAt.HasValue)
|
||||
{
|
||||
feed.Token = token;
|
||||
feed.TokenHash = tokenHash;
|
||||
feed.RevokedAt = null;
|
||||
feed.UpdatedAt = now;
|
||||
}
|
||||
else
|
||||
{
|
||||
token = string.Empty;
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
await SendAsync(UserCalendarExportFeedMapper.ToDto(feed, UserCalendarExportFeedMapper.BuildFeedUrl(feed, token)), cancellation: ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class RegenerateUserCalendarExportFeedHandler(AppDbContext dbContext)
|
||||
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/calendar-integrations/export-feed/regenerate");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
Guid userId = User.GetUserId();
|
||||
string token = CalendarExportFeedTokenService.GenerateToken();
|
||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||
|
||||
UserCalendarExportFeed? feed = await dbContext.UserCalendarExportFeeds
|
||||
.SingleOrDefaultAsync(candidate => candidate.UserId == userId, ct);
|
||||
|
||||
if (feed is null)
|
||||
{
|
||||
feed = new UserCalendarExportFeed
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
UpdatedAt = now,
|
||||
};
|
||||
dbContext.UserCalendarExportFeeds.Add(feed);
|
||||
}
|
||||
|
||||
feed.TokenHash = CalendarExportFeedTokenService.HashToken(token);
|
||||
feed.Token = token;
|
||||
feed.RevokedAt = null;
|
||||
feed.UpdatedAt = now;
|
||||
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
await SendAsync(UserCalendarExportFeedMapper.ToDto(feed, UserCalendarExportFeedMapper.BuildFeedUrl(feed, token)), cancellation: ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class RevokeUserCalendarExportFeedHandler(AppDbContext dbContext)
|
||||
: EndpointWithoutRequest<UserCalendarExportFeedDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Delete("/api/calendar-integrations/export-feed");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
UserCalendarExportFeed? feed = await dbContext.UserCalendarExportFeeds
|
||||
.SingleOrDefaultAsync(candidate => candidate.UserId == User.GetUserId(), ct);
|
||||
|
||||
if (feed is not null)
|
||||
{
|
||||
feed.TokenHash = null;
|
||||
feed.Token = null;
|
||||
feed.RevokedAt = DateTimeOffset.UtcNow;
|
||||
feed.UpdatedAt = feed.RevokedAt.Value;
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
await SendAsync(UserCalendarExportFeedMapper.ToDto(feed, null), cancellation: ct);
|
||||
}
|
||||
}
|
||||
|
||||
public class GetUserCalendarExportFeedIcsHandler(
|
||||
AppDbContext dbContext,
|
||||
CalendarExportFeedService feedService)
|
||||
: EndpointWithoutRequest
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
AllowAnonymous();
|
||||
Get("/api/calendar-integrations/export-feed/{token}.ics");
|
||||
Options(o => o.WithTags("Calendar Integrations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
string? token = Route<string?>("token");
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
string tokenHash = CalendarExportFeedTokenService.HashToken(token);
|
||||
|
||||
UserCalendarExportFeed? feed = await dbContext.UserCalendarExportFeeds
|
||||
.SingleOrDefaultAsync(candidate =>
|
||||
candidate.TokenHash == tokenHash &&
|
||||
!candidate.RevokedAt.HasValue,
|
||||
ct);
|
||||
|
||||
if (feed is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
User? user = await dbContext.Users.SingleOrDefaultAsync(candidate => candidate.Id == feed.UserId, ct);
|
||||
if (user is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
string appBaseUrl = $"{HttpContext.Request.Scheme}://{HttpContext.Request.Host}";
|
||||
string ics = await feedService.BuildUserFeedAsync(feed.UserId, user.Email, appBaseUrl, ct);
|
||||
|
||||
HttpContext.Response.ContentType = "text/calendar; charset=utf-8";
|
||||
await HttpContext.Response.WriteAsync(ics, ct);
|
||||
}
|
||||
}
|
||||
|
||||
file static class UserCalendarExportFeedMapper
|
||||
{
|
||||
public static UserCalendarExportFeedDto ToDto(UserCalendarExportFeed? feed, string? feedUrl)
|
||||
{
|
||||
return new UserCalendarExportFeedDto(
|
||||
feed?.TokenHash is not null && !feed.RevokedAt.HasValue,
|
||||
feedUrl,
|
||||
feed?.CreatedAt,
|
||||
feed?.UpdatedAt,
|
||||
feed?.RevokedAt);
|
||||
}
|
||||
|
||||
public static string? BuildFeedUrl(UserCalendarExportFeed? feed, string? token = null)
|
||||
{
|
||||
if (feed?.TokenHash is null || feed.RevokedAt.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string effectiveToken = string.IsNullOrWhiteSpace(token) ? feed.Token ?? string.Empty : token;
|
||||
return string.IsNullOrWhiteSpace(effectiveToken)
|
||||
? null
|
||||
: $"/api/calendar-integrations/export-feed/{effectiveToken}.ics";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Text;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
public sealed record CalendarExportFeedEvent(
|
||||
string Uid,
|
||||
string Title,
|
||||
DateTimeOffset StartsAt,
|
||||
DateTimeOffset EndsAt,
|
||||
bool IsAllDay,
|
||||
string? Description,
|
||||
string? Url);
|
||||
|
||||
public class CalendarExportFeedBuilder
|
||||
{
|
||||
public string Build(string calendarName, IReadOnlyCollection<CalendarExportFeedEvent> events)
|
||||
{
|
||||
StringBuilder builder = new();
|
||||
builder.AppendLine("BEGIN:VCALENDAR");
|
||||
builder.AppendLine("VERSION:2.0");
|
||||
builder.AppendLine("PRODID:-//Socialize//User Work Calendar//EN");
|
||||
builder.AppendLine("CALSCALE:GREGORIAN");
|
||||
builder.AppendLine("METHOD:PUBLISH");
|
||||
builder.AppendLine($"X-WR-CALNAME:{EscapeText(calendarName)}");
|
||||
|
||||
foreach (CalendarExportFeedEvent feedEvent in events.OrderBy(calendarEvent => calendarEvent.StartsAt))
|
||||
{
|
||||
builder.AppendLine("BEGIN:VEVENT");
|
||||
builder.AppendLine($"UID:{EscapeText(feedEvent.Uid)}");
|
||||
builder.AppendLine($"DTSTAMP:{FormatUtc(DateTimeOffset.UtcNow)}");
|
||||
builder.AppendLine($"SUMMARY:{EscapeText(feedEvent.Title)}");
|
||||
|
||||
if (feedEvent.IsAllDay)
|
||||
{
|
||||
builder.AppendLine($"DTSTART;VALUE=DATE:{FormatDate(feedEvent.StartsAt)}");
|
||||
builder.AppendLine($"DTEND;VALUE=DATE:{FormatDate(feedEvent.EndsAt)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AppendLine($"DTSTART:{FormatUtc(feedEvent.StartsAt)}");
|
||||
builder.AppendLine($"DTEND:{FormatUtc(feedEvent.EndsAt)}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(feedEvent.Description))
|
||||
{
|
||||
builder.AppendLine($"DESCRIPTION:{EscapeText(feedEvent.Description)}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(feedEvent.Url))
|
||||
{
|
||||
builder.AppendLine($"URL:{EscapeText(feedEvent.Url)}");
|
||||
}
|
||||
|
||||
builder.AppendLine("END:VEVENT");
|
||||
}
|
||||
|
||||
builder.AppendLine("END:VCALENDAR");
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string FormatDate(DateTimeOffset value)
|
||||
{
|
||||
return value.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string FormatUtc(DateTimeOffset value)
|
||||
{
|
||||
return value.UtcDateTime.ToString("yyyyMMdd'T'HHmmss'Z'", System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static string EscapeText(string value)
|
||||
{
|
||||
return value
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("\r\n", "\\n")
|
||||
.Replace("\n", "\\n")
|
||||
.Replace(";", "\\;")
|
||||
.Replace(",", "\\,");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
public class CalendarExportFeedService(AppDbContext dbContext, CalendarExportFeedBuilder feedBuilder)
|
||||
{
|
||||
public async Task<string> BuildUserFeedAsync(Guid userId, string? userEmail, string appBaseUrl, CancellationToken ct)
|
||||
{
|
||||
string normalizedEmail = userEmail?.Trim().ToUpperInvariant() ?? string.Empty;
|
||||
Guid[] workspaceIds = await dbContext.Workspaces
|
||||
.Where(workspace =>
|
||||
workspace.OwnerUserId == userId ||
|
||||
dbContext.OrganizationMemberships.Any(membership =>
|
||||
membership.OrganizationId == workspace.OrganizationId &&
|
||||
membership.UserId == userId))
|
||||
.Select(workspace => workspace.Id)
|
||||
.ToArrayAsync(ct);
|
||||
|
||||
List<CalendarExportFeedEvent> events = [];
|
||||
|
||||
events.AddRange(await dbContext.ContentItems
|
||||
.Where(item => workspaceIds.Contains(item.WorkspaceId) && item.DueDate.HasValue)
|
||||
.Join(
|
||||
dbContext.Workspaces,
|
||||
item => item.WorkspaceId,
|
||||
workspace => workspace.Id,
|
||||
(item, workspace) => new { item, workspace })
|
||||
.Join(
|
||||
dbContext.Clients,
|
||||
itemWorkspace => itemWorkspace.item.ClientId,
|
||||
client => client.Id,
|
||||
(itemWorkspace, client) => new { itemWorkspace.item, itemWorkspace.workspace, client })
|
||||
.Join(
|
||||
dbContext.Campaigns,
|
||||
itemWorkspaceClient => itemWorkspaceClient.item.CampaignId,
|
||||
campaign => campaign.Id,
|
||||
(itemWorkspaceClient, campaign) => new { itemWorkspaceClient.item, itemWorkspaceClient.workspace, itemWorkspaceClient.client, campaign })
|
||||
.Select(candidate => ToContentFeedEvent(
|
||||
candidate.item.Id,
|
||||
candidate.item.Title,
|
||||
candidate.item.Status,
|
||||
candidate.item.DueDate!.Value,
|
||||
candidate.workspace.Name,
|
||||
candidate.client.Name,
|
||||
candidate.campaign.Name,
|
||||
appBaseUrl))
|
||||
.ToListAsync(ct));
|
||||
|
||||
events.AddRange(await dbContext.ApprovalRequests
|
||||
.Where(approval =>
|
||||
approval.DueAt.HasValue &&
|
||||
(approval.RequestedByUserId == userId ||
|
||||
(!string.IsNullOrEmpty(normalizedEmail) && approval.ReviewerEmail.ToUpper() == normalizedEmail)))
|
||||
.Join(
|
||||
dbContext.ContentItems,
|
||||
approval => approval.ContentItemId,
|
||||
item => item.Id,
|
||||
(approval, item) => new { approval, item })
|
||||
.Where(candidate => workspaceIds.Contains(candidate.approval.WorkspaceId))
|
||||
.Join(
|
||||
dbContext.Workspaces,
|
||||
approvalItem => approvalItem.approval.WorkspaceId,
|
||||
workspace => workspace.Id,
|
||||
(approvalItem, workspace) => new { approvalItem.approval, approvalItem.item, workspace })
|
||||
.Select(candidate => ToApprovalFeedEvent(
|
||||
candidate.approval.Id,
|
||||
candidate.item.Id,
|
||||
candidate.item.Title,
|
||||
candidate.approval.Stage,
|
||||
candidate.approval.State,
|
||||
candidate.approval.DueAt!.Value,
|
||||
candidate.workspace.Name,
|
||||
appBaseUrl))
|
||||
.ToListAsync(ct));
|
||||
|
||||
events.AddRange(await dbContext.Campaigns
|
||||
.Where(campaign => workspaceIds.Contains(campaign.WorkspaceId))
|
||||
.Join(
|
||||
dbContext.Workspaces,
|
||||
campaign => campaign.WorkspaceId,
|
||||
workspace => workspace.Id,
|
||||
(campaign, workspace) => new { campaign, workspace })
|
||||
.Select(candidate => ToCampaignFeedEvent(
|
||||
candidate.campaign.Id,
|
||||
candidate.campaign.Name,
|
||||
candidate.campaign.Status,
|
||||
candidate.campaign.StartDate,
|
||||
candidate.campaign.EndDate,
|
||||
candidate.workspace.Name,
|
||||
appBaseUrl))
|
||||
.ToListAsync(ct));
|
||||
|
||||
return feedBuilder.Build("Socialize my work", events);
|
||||
}
|
||||
|
||||
private static CalendarExportFeedEvent ToContentFeedEvent(
|
||||
Guid contentItemId,
|
||||
string title,
|
||||
string status,
|
||||
DateTimeOffset dueDate,
|
||||
string workspaceName,
|
||||
string clientName,
|
||||
string campaignName,
|
||||
string appBaseUrl)
|
||||
{
|
||||
(DateTimeOffset start, DateTimeOffset end, bool isAllDay) = NormalizeEventTime(dueDate);
|
||||
|
||||
return new CalendarExportFeedEvent(
|
||||
$"content-{contentItemId}@socialize",
|
||||
title,
|
||||
start,
|
||||
end,
|
||||
isAllDay,
|
||||
$"Status: {status}\nWorkspace: {workspaceName}\nClient: {clientName}\nCampaign: {campaignName}",
|
||||
$"{appBaseUrl.TrimEnd('/')}/app/content/{contentItemId}");
|
||||
}
|
||||
|
||||
private static CalendarExportFeedEvent ToApprovalFeedEvent(
|
||||
Guid approvalId,
|
||||
Guid contentItemId,
|
||||
string contentTitle,
|
||||
string stage,
|
||||
string state,
|
||||
DateTimeOffset dueAt,
|
||||
string workspaceName,
|
||||
string appBaseUrl)
|
||||
{
|
||||
(DateTimeOffset start, DateTimeOffset end, bool isAllDay) = NormalizeEventTime(dueAt);
|
||||
|
||||
return new CalendarExportFeedEvent(
|
||||
$"approval-{approvalId}@socialize",
|
||||
$"Approval due: {contentTitle}",
|
||||
start,
|
||||
end,
|
||||
isAllDay,
|
||||
$"Stage: {stage}\nState: {state}\nWorkspace: {workspaceName}",
|
||||
$"{appBaseUrl.TrimEnd('/')}/app/content/{contentItemId}");
|
||||
}
|
||||
|
||||
private static CalendarExportFeedEvent ToCampaignFeedEvent(
|
||||
Guid campaignId,
|
||||
string name,
|
||||
string status,
|
||||
DateTimeOffset startDate,
|
||||
DateTimeOffset endDate,
|
||||
string workspaceName,
|
||||
string appBaseUrl)
|
||||
{
|
||||
DateTimeOffset start = new(startDate.Date, startDate.Offset);
|
||||
DateTimeOffset end = new(endDate.Date.AddDays(1), endDate.Offset);
|
||||
|
||||
return new CalendarExportFeedEvent(
|
||||
$"campaign-{campaignId}@socialize",
|
||||
$"Campaign: {name}",
|
||||
start,
|
||||
end <= start ? start.AddDays(1) : end,
|
||||
true,
|
||||
$"Status: {status}\nWorkspace: {workspaceName}",
|
||||
$"{appBaseUrl.TrimEnd('/')}/app/campaigns/{campaignId}");
|
||||
}
|
||||
|
||||
private static (DateTimeOffset Start, DateTimeOffset End, bool IsAllDay) NormalizeEventTime(DateTimeOffset value)
|
||||
{
|
||||
if (value.TimeOfDay == TimeSpan.Zero)
|
||||
{
|
||||
DateTimeOffset start = new(value.Date, value.Offset);
|
||||
return (start, start.AddDays(1), true);
|
||||
}
|
||||
|
||||
return (value, value.AddMinutes(30), false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
public static class CalendarExportFeedTokenService
|
||||
{
|
||||
public static string GenerateToken()
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[32];
|
||||
RandomNumberGenerator.Fill(bytes);
|
||||
return Base64UrlEncode(bytes);
|
||||
}
|
||||
|
||||
public static string HashToken(string token)
|
||||
{
|
||||
byte[] bytes = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(token));
|
||||
return Convert.ToHexString(bytes);
|
||||
}
|
||||
|
||||
private static string Base64UrlEncode(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
return Convert.ToBase64String(bytes)
|
||||
.TrimEnd('=')
|
||||
.Replace('+', '-')
|
||||
.Replace('/', '_');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
public sealed class CalendarImportBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<CalendarImportBackgroundService> logger)
|
||||
: BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
using PeriodicTimer timer = new(TimeSpan.FromHours(6));
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
await RefreshDueSourcesAsync(stoppingToken);
|
||||
await timer.WaitForNextTickAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshDueSourcesAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using IServiceScope scope = scopeFactory.CreateScope();
|
||||
CalendarImportSyncService syncService = scope.ServiceProvider.GetRequiredService<CalendarImportSyncService>();
|
||||
await syncService.RefreshDueSourcesAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Calendar import background sync failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
public sealed class CalendarImportSyncService(
|
||||
AppDbContext dbContext,
|
||||
IHttpClientFactory httpClientFactory,
|
||||
IcsCalendarParser parser)
|
||||
{
|
||||
public async Task RefreshSourceAsync(Guid sourceId, CancellationToken ct)
|
||||
{
|
||||
CalendarSource? source = await dbContext.CalendarSources
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == sourceId, ct);
|
||||
if (source is null)
|
||||
{
|
||||
throw new InvalidOperationException("Calendar source was not found.");
|
||||
}
|
||||
|
||||
source.LastAttemptedSyncAt = DateTimeOffset.UtcNow;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(source.SourceUrl))
|
||||
{
|
||||
source.LastSyncError = "Calendar source does not have a source URL.";
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using HttpClient httpClient = httpClientFactory.CreateClient();
|
||||
DateOnly rangeStart = DateOnly.FromDateTime(DateTime.UtcNow.AddYears(-1));
|
||||
DateOnly rangeEnd = DateOnly.FromDateTime(DateTime.UtcNow.AddYears(2));
|
||||
IReadOnlyCollection<ParsedCalendarEvent> parsedEvents = await GetParsedEventsAsync(
|
||||
httpClient,
|
||||
source.SourceUrl,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
ct);
|
||||
|
||||
await ReplaceEventsAsync(source.Id, parsedEvents, ct);
|
||||
|
||||
source.LastSuccessfulSyncAt = DateTimeOffset.UtcNow;
|
||||
source.LastSyncError = null;
|
||||
source.LastAttemptedSyncAt = source.LastSuccessfulSyncAt;
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
await RecordSyncFailureAsync(source, ex.Message, ct);
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
await RecordSyncFailureAsync(source, ex.Message, ct);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
await RecordSyncFailureAsync(source, ex.Message, ct);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RefreshDueSourcesAsync(CancellationToken ct)
|
||||
{
|
||||
DateTimeOffset staleBefore = DateTimeOffset.UtcNow.AddHours(-12);
|
||||
Guid[] sourceIds = await dbContext.CalendarSources
|
||||
.Where(source => source.IsEnabled && source.SourceUrl != null)
|
||||
.Where(source => source.LastAttemptedSyncAt == null || source.LastAttemptedSyncAt < staleBefore)
|
||||
.OrderBy(source => source.LastAttemptedSyncAt)
|
||||
.Select(source => source.Id)
|
||||
.Take(25)
|
||||
.ToArrayAsync(ct);
|
||||
|
||||
foreach (Guid sourceId in sourceIds)
|
||||
{
|
||||
await RefreshSourceAsync(sourceId, ct);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReplaceEventsAsync(
|
||||
Guid sourceId,
|
||||
IReadOnlyCollection<ParsedCalendarEvent> parsedEvents,
|
||||
CancellationToken ct)
|
||||
{
|
||||
await dbContext.CalendarEvents
|
||||
.Where(calendarEvent => calendarEvent.CalendarSourceId == sourceId)
|
||||
.ExecuteDeleteAsync(ct);
|
||||
|
||||
DateTimeOffset importedAt = DateTimeOffset.UtcNow;
|
||||
foreach (ParsedCalendarEvent parsedEvent in parsedEvents)
|
||||
{
|
||||
dbContext.CalendarEvents.Add(new CalendarEvent
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarSourceId = sourceId,
|
||||
SourceEventUid = parsedEvent.SourceEventUid,
|
||||
Title = parsedEvent.Title,
|
||||
Description = parsedEvent.Description,
|
||||
IsAllDay = parsedEvent.IsAllDay,
|
||||
IsFloatingTime = parsedEvent.IsFloatingTime,
|
||||
StartDate = parsedEvent.StartDate,
|
||||
EndDate = parsedEvent.EndDate,
|
||||
StartLocalDateTime = parsedEvent.StartLocalDateTime,
|
||||
EndLocalDateTime = parsedEvent.EndLocalDateTime,
|
||||
StartUtc = parsedEvent.StartUtc,
|
||||
EndUtc = parsedEvent.EndUtc,
|
||||
TimeZoneId = parsedEvent.TimeZoneId,
|
||||
RecurrenceId = parsedEvent.RecurrenceId,
|
||||
Location = parsedEvent.Location,
|
||||
SourceUrl = parsedEvent.SourceUrl,
|
||||
SourceLastModifiedAt = parsedEvent.SourceLastModifiedAt,
|
||||
ImportedAt = importedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyCollection<ParsedCalendarEvent>> GetParsedEventsAsync(
|
||||
HttpClient httpClient,
|
||||
string sourceUrl,
|
||||
DateOnly rangeStart,
|
||||
DateOnly rangeEnd,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (TryGetNagerCountryCode(sourceUrl, out string? countryCode))
|
||||
{
|
||||
return await GetNagerEventsAsync(httpClient, sourceUrl, countryCode!, rangeStart, rangeEnd, ct);
|
||||
}
|
||||
|
||||
string content = await httpClient.GetStringAsync(sourceUrl, ct);
|
||||
return parser.Parse(content, rangeStart, rangeEnd);
|
||||
}
|
||||
|
||||
private static async Task<IReadOnlyCollection<ParsedCalendarEvent>> GetNagerEventsAsync(
|
||||
HttpClient httpClient,
|
||||
string sourceUrl,
|
||||
string countryCode,
|
||||
DateOnly rangeStart,
|
||||
DateOnly rangeEnd,
|
||||
CancellationToken ct)
|
||||
{
|
||||
List<ParsedCalendarEvent> events = [];
|
||||
for (int year = rangeStart.Year; year <= rangeEnd.Year; year++)
|
||||
{
|
||||
string yearUrl = BuildNagerYearUrl(sourceUrl, countryCode, year);
|
||||
string json = await httpClient.GetStringAsync(yearUrl, ct);
|
||||
NagerHoliday[] holidays = JsonSerializer.Deserialize<NagerHoliday[]>(
|
||||
json,
|
||||
new JsonSerializerOptions(JsonSerializerDefaults.Web)) ?? [];
|
||||
|
||||
foreach (NagerHoliday holiday in holidays)
|
||||
{
|
||||
if (!DateOnly.TryParse(holiday.Date, out DateOnly date) ||
|
||||
date < rangeStart ||
|
||||
date > rangeEnd)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
events.Add(ToParsedEvent(
|
||||
$"nager-{countryCode}-{date:yyyyMMdd}-{NormalizeUidPart(holiday.Name)}",
|
||||
string.IsNullOrWhiteSpace(holiday.Name) ? holiday.LocalName : holiday.Name,
|
||||
holiday.LocalName,
|
||||
date,
|
||||
string.Join(", ", holiday.Types ?? []),
|
||||
yearUrl));
|
||||
}
|
||||
|
||||
events.AddRange(GetSupplementalCountryEvents(countryCode, year, rangeStart, rangeEnd));
|
||||
}
|
||||
|
||||
return events
|
||||
.GroupBy(calendarEvent => calendarEvent.SourceEventUid)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(calendarEvent => calendarEvent.StartDate)
|
||||
.ThenBy(calendarEvent => calendarEvent.Title)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static ParsedCalendarEvent ToParsedEvent(
|
||||
string uid,
|
||||
string? title,
|
||||
string? localName,
|
||||
DateOnly date,
|
||||
string? types,
|
||||
string sourceUrl)
|
||||
{
|
||||
string? description = string.IsNullOrWhiteSpace(types)
|
||||
? localName
|
||||
: $"{localName}\nTypes: {types}";
|
||||
|
||||
return new ParsedCalendarEvent(
|
||||
uid,
|
||||
string.IsNullOrWhiteSpace(title) ? "Untitled event" : title,
|
||||
description,
|
||||
IsAllDay: true,
|
||||
IsFloatingTime: false,
|
||||
date,
|
||||
date.AddDays(1),
|
||||
StartLocalDateTime: null,
|
||||
EndLocalDateTime: null,
|
||||
StartUtc: null,
|
||||
EndUtc: null,
|
||||
TimeZoneId: null,
|
||||
RecurrenceId: null,
|
||||
Location: null,
|
||||
sourceUrl,
|
||||
SourceLastModifiedAt: null);
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<ParsedCalendarEvent> GetSupplementalCountryEvents(
|
||||
string countryCode,
|
||||
int year,
|
||||
DateOnly rangeStart,
|
||||
DateOnly rangeEnd)
|
||||
{
|
||||
if (!countryCode.Equals("CA", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
DateOnly mothersDay = NthWeekdayOfMonth(year, month: 5, DayOfWeek.Sunday, occurrence: 2);
|
||||
if (mothersDay < rangeStart || mothersDay > rangeEnd)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return
|
||||
[
|
||||
ToParsedEvent(
|
||||
$"socialize-ca-mothers-day-{year}",
|
||||
"Mother's Day",
|
||||
"Mother's Day",
|
||||
mothersDay,
|
||||
"Observance",
|
||||
"socialize://calendar-observances/CA"),
|
||||
];
|
||||
}
|
||||
|
||||
private static DateOnly NthWeekdayOfMonth(int year, int month, DayOfWeek dayOfWeek, int occurrence)
|
||||
{
|
||||
DateOnly date = new(year, month, 1);
|
||||
while (date.DayOfWeek != dayOfWeek)
|
||||
{
|
||||
date = date.AddDays(1);
|
||||
}
|
||||
|
||||
return date.AddDays((occurrence - 1) * 7);
|
||||
}
|
||||
|
||||
private static bool TryGetNagerCountryCode(string sourceUrl, out string? countryCode)
|
||||
{
|
||||
countryCode = null;
|
||||
if (!Uri.TryCreate(sourceUrl, UriKind.Absolute, out Uri? uri) ||
|
||||
!uri.Host.Contains("date.nager.at", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] segments = uri.AbsolutePath
|
||||
.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
string? candidate = segments.LastOrDefault(segment => segment.Length == 2);
|
||||
if (candidate is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
countryCode = candidate.ToUpperInvariant();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string BuildNagerYearUrl(string sourceUrl, string countryCode, int year)
|
||||
{
|
||||
if (Uri.TryCreate(sourceUrl, UriKind.Absolute, out Uri? uri))
|
||||
{
|
||||
return $"{uri.Scheme}://{uri.Host}/api/v3/PublicHolidays/{year}/{countryCode}";
|
||||
}
|
||||
|
||||
return $"https://date.nager.at/api/v3/PublicHolidays/{year}/{countryCode}";
|
||||
}
|
||||
|
||||
private static string NormalizeUidPart(string? value)
|
||||
{
|
||||
return new string((value ?? "holiday")
|
||||
.ToLowerInvariant()
|
||||
.Select(character => char.IsLetterOrDigit(character) ? character : '-')
|
||||
.ToArray())
|
||||
.Trim('-');
|
||||
}
|
||||
|
||||
private async Task RecordSyncFailureAsync(
|
||||
CalendarSource source,
|
||||
string message,
|
||||
CancellationToken ct)
|
||||
{
|
||||
source.LastSyncError = NormalizeSyncError(message);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public static string NormalizeSyncError(string message)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(message);
|
||||
|
||||
return message.Length > 2048 ? message[..2048] : message;
|
||||
}
|
||||
|
||||
private sealed record NagerHoliday(
|
||||
string Date,
|
||||
string LocalName,
|
||||
string Name,
|
||||
string[]? Types);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
public static class CalendarSourceScopes
|
||||
{
|
||||
public const string Organization = "Organization";
|
||||
public const string Workspace = "Workspace";
|
||||
public const string User = "User";
|
||||
}
|
||||
|
||||
public static class CalendarSourceInheritanceModes
|
||||
{
|
||||
public const string Required = "Required";
|
||||
public const string Optional = "Optional";
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
public static class CalendarSourceRules
|
||||
{
|
||||
public static readonly string[] SupportedScopes =
|
||||
[
|
||||
CalendarSourceScopes.Organization,
|
||||
CalendarSourceScopes.Workspace,
|
||||
CalendarSourceScopes.User,
|
||||
];
|
||||
|
||||
public static readonly string[] SupportedInheritanceModes =
|
||||
[
|
||||
CalendarSourceInheritanceModes.Required,
|
||||
CalendarSourceInheritanceModes.Optional,
|
||||
];
|
||||
|
||||
public static bool IsSupportedScope(string? scope)
|
||||
{
|
||||
return SupportedScopes.Contains(scope?.Trim(), StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public static bool IsSupportedInheritanceMode(string? inheritanceMode)
|
||||
{
|
||||
return SupportedInheritanceModes.Contains(inheritanceMode?.Trim(), StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
public static bool IsInheritedOrganizationSource(CalendarSource source, Guid workspaceOrganizationId)
|
||||
{
|
||||
return source.Scope == CalendarSourceScopes.Organization &&
|
||||
source.OrganizationId == workspaceOrganizationId;
|
||||
}
|
||||
|
||||
public static bool CanManageScope(
|
||||
string scope,
|
||||
bool canManageOrganizationCalendars,
|
||||
bool canManageWorkspaceCalendars,
|
||||
Guid currentUserId,
|
||||
Guid? sourceUserId)
|
||||
{
|
||||
return scope switch
|
||||
{
|
||||
CalendarSourceScopes.Organization => canManageOrganizationCalendars,
|
||||
CalendarSourceScopes.Workspace => canManageWorkspaceCalendars,
|
||||
CalendarSourceScopes.User => sourceUserId == currentUserId,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,414 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||
|
||||
public record ParsedCalendarEvent(
|
||||
string SourceEventUid,
|
||||
string Title,
|
||||
string? Description,
|
||||
bool IsAllDay,
|
||||
bool IsFloatingTime,
|
||||
DateOnly StartDate,
|
||||
DateOnly EndDate,
|
||||
DateTime? StartLocalDateTime,
|
||||
DateTime? EndLocalDateTime,
|
||||
DateTimeOffset? StartUtc,
|
||||
DateTimeOffset? EndUtc,
|
||||
string? TimeZoneId,
|
||||
string? RecurrenceId,
|
||||
string? Location,
|
||||
string? SourceUrl,
|
||||
DateTimeOffset? SourceLastModifiedAt);
|
||||
|
||||
internal record IcsDateTimeValue(
|
||||
bool IsAllDay,
|
||||
bool IsFloatingTime,
|
||||
DateOnly Date,
|
||||
DateTime? LocalDateTime,
|
||||
DateTimeOffset? UtcDateTime,
|
||||
string? TimeZoneId);
|
||||
|
||||
internal sealed record IcsRawEvent(
|
||||
string Uid,
|
||||
string Title,
|
||||
string? Description,
|
||||
IcsDateTimeValue Start,
|
||||
IcsDateTimeValue? End,
|
||||
string? RRule,
|
||||
string? Location,
|
||||
string? SourceUrl,
|
||||
DateTimeOffset? LastModifiedAt);
|
||||
|
||||
public sealed class IcsCalendarParser
|
||||
{
|
||||
public IReadOnlyCollection<ParsedCalendarEvent> Parse(
|
||||
string content,
|
||||
DateOnly rangeStart,
|
||||
DateOnly rangeEnd)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(content);
|
||||
|
||||
List<ParsedCalendarEvent> events = [];
|
||||
foreach (IcsRawEvent rawEvent in ReadRawEvents(content))
|
||||
{
|
||||
events.AddRange(Expand(rawEvent, rangeStart, rangeEnd));
|
||||
}
|
||||
|
||||
return events
|
||||
.OrderBy(calendarEvent => calendarEvent.StartDate)
|
||||
.ThenBy(calendarEvent => calendarEvent.Title)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<IcsRawEvent> ReadRawEvents(string content)
|
||||
{
|
||||
List<string> lines = UnfoldLines(content).ToList();
|
||||
for (int index = 0; index < lines.Count; index++)
|
||||
{
|
||||
if (!lines[index].Equals("BEGIN:VEVENT", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Dictionary<string, List<(Dictionary<string, string> Parameters, string Value)>> properties =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
index++;
|
||||
for (; index < lines.Count && !lines[index].Equals("END:VEVENT", StringComparison.OrdinalIgnoreCase); index++)
|
||||
{
|
||||
ParseProperty(lines[index], properties);
|
||||
}
|
||||
|
||||
if (!TryGetFirst(properties, "DTSTART", out var startProperty))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IcsDateTimeValue start = ParseDateTimeValue(startProperty.Value, startProperty.Parameters);
|
||||
IcsDateTimeValue? end = TryGetFirst(properties, "DTEND", out var endProperty)
|
||||
? ParseDateTimeValue(endProperty.Value, endProperty.Parameters)
|
||||
: null;
|
||||
|
||||
string uid = TryGetFirst(properties, "UID", out var uidProperty)
|
||||
? uidProperty.Value
|
||||
: $"{start.Date:yyyyMMdd}:{GetText(properties, "SUMMARY") ?? "calendar-event"}";
|
||||
|
||||
yield return new IcsRawEvent(
|
||||
uid,
|
||||
GetText(properties, "SUMMARY") ?? "Untitled event",
|
||||
GetText(properties, "DESCRIPTION"),
|
||||
start,
|
||||
end,
|
||||
GetText(properties, "RRULE"),
|
||||
GetText(properties, "LOCATION"),
|
||||
GetText(properties, "URL"),
|
||||
TryGetFirst(properties, "LAST-MODIFIED", out var lastModified)
|
||||
? ParseDateTimeValue(lastModified.Value, lastModified.Parameters).UtcDateTime
|
||||
: null);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<string> UnfoldLines(string content)
|
||||
{
|
||||
string? current = null;
|
||||
using StringReader reader = new(content.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n'));
|
||||
while (reader.ReadLine() is { } line)
|
||||
{
|
||||
if ((line.StartsWith(' ') || line.StartsWith('\t')) && current is not null)
|
||||
{
|
||||
current += line[1..];
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current is not null)
|
||||
{
|
||||
yield return current;
|
||||
}
|
||||
|
||||
current = line;
|
||||
}
|
||||
|
||||
if (current is not null)
|
||||
{
|
||||
yield return current;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseProperty(
|
||||
string line,
|
||||
Dictionary<string, List<(Dictionary<string, string> Parameters, string Value)>> properties)
|
||||
{
|
||||
int separatorIndex = line.IndexOf(':', StringComparison.Ordinal);
|
||||
if (separatorIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string nameAndParameters = line[..separatorIndex];
|
||||
string value = UnescapeText(line[(separatorIndex + 1)..]);
|
||||
string[] nameParts = nameAndParameters.Split(';');
|
||||
string name = nameParts[0];
|
||||
Dictionary<string, string> parameters = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (string parameterPart in nameParts.Skip(1))
|
||||
{
|
||||
int equalsIndex = parameterPart.IndexOf('=', StringComparison.Ordinal);
|
||||
if (equalsIndex > 0)
|
||||
{
|
||||
parameters[parameterPart[..equalsIndex]] = parameterPart[(equalsIndex + 1)..].Trim('"');
|
||||
}
|
||||
}
|
||||
|
||||
if (!properties.TryGetValue(name, out var values))
|
||||
{
|
||||
values = [];
|
||||
properties[name] = values;
|
||||
}
|
||||
|
||||
values.Add((parameters, value));
|
||||
}
|
||||
|
||||
private static string UnescapeText(string value)
|
||||
{
|
||||
return value
|
||||
.Replace("\\n", "\n", StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("\\,", ",", StringComparison.Ordinal)
|
||||
.Replace("\\;", ";", StringComparison.Ordinal)
|
||||
.Replace("\\\\", "\\", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool TryGetFirst(
|
||||
Dictionary<string, List<(Dictionary<string, string> Parameters, string Value)>> properties,
|
||||
string key,
|
||||
out (Dictionary<string, string> Parameters, string Value) value)
|
||||
{
|
||||
if (properties.TryGetValue(key, out var values) && values.Count > 0)
|
||||
{
|
||||
value = values[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string? GetText(
|
||||
Dictionary<string, List<(Dictionary<string, string> Parameters, string Value)>> properties,
|
||||
string key)
|
||||
{
|
||||
return TryGetFirst(properties, key, out var value) ? value.Value : null;
|
||||
}
|
||||
|
||||
private static IcsDateTimeValue ParseDateTimeValue(
|
||||
string value,
|
||||
Dictionary<string, string> parameters)
|
||||
{
|
||||
bool isAllDay = parameters.TryGetValue("VALUE", out string? valueType) &&
|
||||
valueType.Equals("DATE", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (isAllDay || value.Length == 8)
|
||||
{
|
||||
DateOnly date = DateOnly.ParseExact(value, "yyyyMMdd", CultureInfo.InvariantCulture);
|
||||
return new IcsDateTimeValue(true, false, date, null, null, null);
|
||||
}
|
||||
|
||||
bool utc = value.EndsWith('Z');
|
||||
string parseValue = utc ? value[..^1] : value;
|
||||
DateTime local = DateTime.ParseExact(parseValue, "yyyyMMdd'T'HHmmss", CultureInfo.InvariantCulture);
|
||||
if (utc)
|
||||
{
|
||||
DateTimeOffset utcValue = new(DateTime.SpecifyKind(local, DateTimeKind.Utc));
|
||||
return new IcsDateTimeValue(false, false, DateOnly.FromDateTime(local), local, utcValue, "UTC");
|
||||
}
|
||||
|
||||
string? timeZoneId = parameters.GetValueOrDefault("TZID");
|
||||
bool floating = string.IsNullOrWhiteSpace(timeZoneId);
|
||||
return new IcsDateTimeValue(
|
||||
false,
|
||||
floating,
|
||||
DateOnly.FromDateTime(local),
|
||||
local,
|
||||
TryConvertToUtc(local, timeZoneId),
|
||||
timeZoneId);
|
||||
}
|
||||
|
||||
private static DateTimeOffset? TryConvertToUtc(DateTime localDateTime, string? timeZoneId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(timeZoneId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
|
||||
DateTime unspecified = DateTime.SpecifyKind(localDateTime, DateTimeKind.Unspecified);
|
||||
return TimeZoneInfo.ConvertTimeToUtc(unspecified, timeZone);
|
||||
}
|
||||
catch (TimeZoneNotFoundException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch (InvalidTimeZoneException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ParsedCalendarEvent> Expand(
|
||||
IcsRawEvent rawEvent,
|
||||
DateOnly rangeStart,
|
||||
DateOnly rangeEnd)
|
||||
{
|
||||
TimeSpan duration = GetDuration(rawEvent.Start, rawEvent.End);
|
||||
IReadOnlyCollection<DateOnly> starts = ExpandStartDates(rawEvent, rangeStart, rangeEnd);
|
||||
foreach (DateOnly startDate in starts)
|
||||
{
|
||||
int dayOffset = startDate.DayNumber - rawEvent.Start.Date.DayNumber;
|
||||
IcsDateTimeValue occurrenceStart = Shift(rawEvent.Start, dayOffset);
|
||||
IcsDateTimeValue occurrenceEnd = rawEvent.End is null
|
||||
? ShiftByDuration(occurrenceStart, duration)
|
||||
: Shift(rawEvent.End, dayOffset);
|
||||
|
||||
yield return new ParsedCalendarEvent(
|
||||
rawEvent.Uid,
|
||||
rawEvent.Title,
|
||||
rawEvent.Description,
|
||||
occurrenceStart.IsAllDay,
|
||||
occurrenceStart.IsFloatingTime,
|
||||
occurrenceStart.Date,
|
||||
occurrenceEnd.Date,
|
||||
occurrenceStart.LocalDateTime,
|
||||
occurrenceEnd.LocalDateTime,
|
||||
occurrenceStart.UtcDateTime,
|
||||
occurrenceEnd.UtcDateTime,
|
||||
occurrenceStart.TimeZoneId,
|
||||
rawEvent.RRule is null ? null : rawEvent.Uid,
|
||||
rawEvent.Location,
|
||||
rawEvent.SourceUrl,
|
||||
rawEvent.LastModifiedAt);
|
||||
}
|
||||
}
|
||||
|
||||
private static TimeSpan GetDuration(IcsDateTimeValue start, IcsDateTimeValue? end)
|
||||
{
|
||||
if (end is null)
|
||||
{
|
||||
return start.IsAllDay ? TimeSpan.FromDays(1) : TimeSpan.Zero;
|
||||
}
|
||||
|
||||
if (start.IsAllDay)
|
||||
{
|
||||
return TimeSpan.FromDays(Math.Max(1, end.Date.DayNumber - start.Date.DayNumber));
|
||||
}
|
||||
|
||||
if (start.LocalDateTime.HasValue && end.LocalDateTime.HasValue)
|
||||
{
|
||||
return end.LocalDateTime.Value - start.LocalDateTime.Value;
|
||||
}
|
||||
|
||||
return TimeSpan.Zero;
|
||||
}
|
||||
|
||||
private static IReadOnlyCollection<DateOnly> ExpandStartDates(
|
||||
IcsRawEvent rawEvent,
|
||||
DateOnly rangeStart,
|
||||
DateOnly rangeEnd)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawEvent.RRule))
|
||||
{
|
||||
return IsInRange(rawEvent.Start.Date, rangeStart, rangeEnd) ? [rawEvent.Start.Date] : [];
|
||||
}
|
||||
|
||||
Dictionary<string, string> rule = rawEvent.RRule
|
||||
.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(part => part.Split('=', 2))
|
||||
.Where(parts => parts.Length == 2)
|
||||
.ToDictionary(parts => parts[0], parts => parts[1], StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
string frequency = rule.GetValueOrDefault("FREQ", "DAILY");
|
||||
int interval = int.TryParse(rule.GetValueOrDefault("INTERVAL"), out int parsedInterval)
|
||||
? Math.Max(1, parsedInterval)
|
||||
: 1;
|
||||
int? count = int.TryParse(rule.GetValueOrDefault("COUNT"), out int parsedCount) ? parsedCount : null;
|
||||
DateOnly? until = TryParseUntil(rule.GetValueOrDefault("UNTIL"));
|
||||
List<DateOnly> dates = [];
|
||||
|
||||
DateOnly current = rawEvent.Start.Date;
|
||||
for (int occurrence = 1; occurrence <= (count ?? 500); occurrence++)
|
||||
{
|
||||
if (until.HasValue && current > until.Value)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (current > rangeEnd)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsInRange(current, rangeStart, rangeEnd))
|
||||
{
|
||||
dates.Add(current);
|
||||
}
|
||||
|
||||
current = frequency.ToUpperInvariant() switch
|
||||
{
|
||||
"YEARLY" => current.AddYears(interval),
|
||||
"MONTHLY" => current.AddMonths(interval),
|
||||
"WEEKLY" => current.AddDays(7 * interval),
|
||||
_ => current.AddDays(interval),
|
||||
};
|
||||
}
|
||||
|
||||
return dates;
|
||||
}
|
||||
|
||||
private static DateOnly? TryParseUntil(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string dateValue = value.EndsWith('Z') ? value[..^1] : value;
|
||||
if (dateValue.Length >= 8 &&
|
||||
DateOnly.TryParseExact(dateValue[..8], "yyyyMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateOnly date))
|
||||
{
|
||||
return date;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsInRange(DateOnly value, DateOnly rangeStart, DateOnly rangeEnd)
|
||||
{
|
||||
return value >= rangeStart && value <= rangeEnd;
|
||||
}
|
||||
|
||||
private static IcsDateTimeValue Shift(IcsDateTimeValue value, int dayOffset)
|
||||
{
|
||||
return value with
|
||||
{
|
||||
Date = value.Date.AddDays(dayOffset),
|
||||
LocalDateTime = value.LocalDateTime?.AddDays(dayOffset),
|
||||
UtcDateTime = value.UtcDateTime?.AddDays(dayOffset),
|
||||
};
|
||||
}
|
||||
|
||||
private static IcsDateTimeValue ShiftByDuration(IcsDateTimeValue value, TimeSpan duration)
|
||||
{
|
||||
if (value.IsAllDay)
|
||||
{
|
||||
return value with { Date = value.Date.AddDays(Math.Max(1, (int)duration.TotalDays)) };
|
||||
}
|
||||
|
||||
return value with
|
||||
{
|
||||
Date = value.LocalDateTime.HasValue
|
||||
? DateOnly.FromDateTime(value.LocalDateTime.Value.Add(duration))
|
||||
: value.Date,
|
||||
LocalDateTime = value.LocalDateTime?.Add(duration),
|
||||
UtcDateTime = value.UtcDateTime?.Add(duration),
|
||||
};
|
||||
}
|
||||
}
|
||||
12
backend/src/Socialize.Api/Modules/Channels/Data/Channel.cs
Normal file
12
backend/src/Socialize.Api/Modules/Channels/Data/Channel.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Socialize.Api.Modules.Channels.Data;
|
||||
|
||||
public class Channel
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public Guid WorkspaceId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Network { get; set; }
|
||||
public string? Handle { get; set; }
|
||||
public string? ExternalUrl { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Socialize.Api.Modules.Channels.Data;
|
||||
|
||||
public static class ChannelModelConfiguration
|
||||
{
|
||||
public static ModelBuilder ConfigureChannelsModule(this ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Channel>(channel =>
|
||||
{
|
||||
channel.ToTable("Channels");
|
||||
channel.HasKey(x => x.Id);
|
||||
channel.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||
channel.Property(x => x.Network).HasMaxLength(64).IsRequired();
|
||||
channel.Property(x => x.Handle).HasMaxLength(256);
|
||||
channel.Property(x => x.ExternalUrl).HasMaxLength(2048);
|
||||
channel.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
channel.HasIndex(x => x.WorkspaceId);
|
||||
channel.HasIndex(x => new { x.WorkspaceId, x.Network, x.Name }).IsUnique();
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Socialize.Api.Modules.Channels;
|
||||
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static WebApplicationBuilder AddChannelsModule(
|
||||
this WebApplicationBuilder builder)
|
||||
{
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Socialize.Api.Modules.Channels.Handlers;
|
||||
|
||||
public record ChannelDto(
|
||||
Guid Id,
|
||||
Guid WorkspaceId,
|
||||
string Name,
|
||||
string Network,
|
||||
string? Handle,
|
||||
string? ExternalUrl,
|
||||
DateTimeOffset CreatedAt);
|
||||
@@ -0,0 +1,115 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Channels.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.Channels.Handlers;
|
||||
|
||||
public record CreateChannelRequest(
|
||||
Guid WorkspaceId,
|
||||
string Name,
|
||||
string Network,
|
||||
string? Handle,
|
||||
string? ExternalUrl);
|
||||
|
||||
public class CreateChannelRequestValidator
|
||||
: Validator<CreateChannelRequest>
|
||||
{
|
||||
private static readonly string[] AllowedNetworks =
|
||||
[
|
||||
"Instagram",
|
||||
"TikTok",
|
||||
"Facebook",
|
||||
"LinkedIn",
|
||||
"YouTube",
|
||||
"X",
|
||||
"Reddit",
|
||||
"Website",
|
||||
];
|
||||
|
||||
public CreateChannelRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.WorkspaceId).NotEmpty();
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||
RuleFor(x => x.Network).NotEmpty().Must(network => AllowedNetworks.Contains(network))
|
||||
.WithMessage("Selected network is invalid.");
|
||||
RuleFor(x => x.Handle).MaximumLength(256);
|
||||
RuleFor(x => x.ExternalUrl).MaximumLength(2048);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateChannelHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService)
|
||||
: Endpoint<CreateChannelRequest, ChannelDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/channels");
|
||||
Options(o => o.WithTags("Channels"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateChannelRequest request, CancellationToken ct)
|
||||
{
|
||||
if (!await accessScopeService.CanManageWorkspaceAsync(User, request.WorkspaceId, ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
bool workspaceExists = await dbContext.Workspaces
|
||||
.AnyAsync(workspace => workspace.Id == request.WorkspaceId, ct);
|
||||
|
||||
if (!workspaceExists)
|
||||
{
|
||||
AddError(request => request.WorkspaceId, "The selected workspace does not exist.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
string normalizedName = request.Name.Trim();
|
||||
string normalizedNetwork = request.Network.Trim();
|
||||
string? normalizedHandle = request.Handle?.Trim();
|
||||
string? normalizedExternalUrl = request.ExternalUrl?.Trim();
|
||||
|
||||
bool duplicateChannel = await dbContext.Channels
|
||||
.AnyAsync(
|
||||
channel => channel.WorkspaceId == request.WorkspaceId
|
||||
&& channel.Network == normalizedNetwork
|
||||
&& channel.Name == normalizedName,
|
||||
ct);
|
||||
|
||||
if (duplicateChannel)
|
||||
{
|
||||
AddError(request => request.Name, "A channel with this name already exists for the selected network.");
|
||||
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
Channel channel = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
WorkspaceId = request.WorkspaceId,
|
||||
Name = normalizedName,
|
||||
Network = normalizedNetwork,
|
||||
Handle = string.IsNullOrWhiteSpace(normalizedHandle) ? null : normalizedHandle,
|
||||
ExternalUrl = string.IsNullOrWhiteSpace(normalizedExternalUrl) ? null : normalizedExternalUrl,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
dbContext.Channels.Add(channel);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
ChannelDto dto = new(
|
||||
channel.Id,
|
||||
channel.WorkspaceId,
|
||||
channel.Name,
|
||||
channel.Network,
|
||||
channel.Handle,
|
||||
channel.ExternalUrl,
|
||||
channel.CreatedAt);
|
||||
|
||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Channels.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.Channels.Handlers;
|
||||
|
||||
public record GetChannelsRequest(Guid? WorkspaceId);
|
||||
|
||||
public class GetChannelsHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService)
|
||||
: Endpoint<GetChannelsRequest, IReadOnlyCollection<ChannelDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/channels");
|
||||
Options(o => o.WithTags("Channels"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(GetChannelsRequest request, CancellationToken ct)
|
||||
{
|
||||
IQueryable<Channel> query = dbContext.Channels.AsQueryable();
|
||||
|
||||
if (!accessScopeService.IsManager(User))
|
||||
{
|
||||
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||
query = query.Where(channel => workspaceScopeIds.Contains(channel.WorkspaceId));
|
||||
}
|
||||
|
||||
if (request.WorkspaceId.HasValue)
|
||||
{
|
||||
query = query.Where(channel => channel.WorkspaceId == request.WorkspaceId.Value);
|
||||
}
|
||||
|
||||
List<ChannelDto> channels = await query
|
||||
.OrderBy(channel => channel.Network)
|
||||
.ThenBy(channel => channel.Name)
|
||||
.Select(channel => new ChannelDto(
|
||||
channel.Id,
|
||||
channel.WorkspaceId,
|
||||
channel.Name,
|
||||
channel.Network,
|
||||
channel.Handle,
|
||||
channel.ExternalUrl,
|
||||
channel.CreatedAt))
|
||||
.ToListAsync(ct);
|
||||
|
||||
await SendOkAsync(channels, ct);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,11 @@ public class Comment
|
||||
public required string AuthorDisplayName { get; set; }
|
||||
public required string AuthorEmail { get; set; }
|
||||
public required string Body { get; set; }
|
||||
public bool IsResolved { get; set; }
|
||||
public string? AttachmentFileName { get; set; }
|
||||
public string? AttachmentContentType { get; set; }
|
||||
public long? AttachmentSizeBytes { get; set; }
|
||||
public string? AttachmentBlobContainerName { get; set; }
|
||||
public string? AttachmentBlobName { get; set; }
|
||||
public string? AttachmentBlobUrl { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
public DateTimeOffset? ResolvedAt { get; set; }
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ public static class CommentModelConfiguration
|
||||
comment.Property(x => x.AuthorDisplayName).HasMaxLength(256).IsRequired();
|
||||
comment.Property(x => x.AuthorEmail).HasMaxLength(256).IsRequired();
|
||||
comment.Property(x => x.Body).HasMaxLength(4000).IsRequired();
|
||||
comment.Property(x => x.AttachmentFileName).HasMaxLength(256);
|
||||
comment.Property(x => x.AttachmentContentType).HasMaxLength(128);
|
||||
comment.Property(x => x.AttachmentBlobContainerName).HasMaxLength(128);
|
||||
comment.Property(x => x.AttachmentBlobName).HasMaxLength(512);
|
||||
comment.Property(x => x.AttachmentBlobUrl).HasMaxLength(1024);
|
||||
comment.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.Comments.Handlers;
|
||||
|
||||
@@ -12,7 +15,8 @@ public record CreateCommentRequest(
|
||||
Guid WorkspaceId,
|
||||
Guid ContentItemId,
|
||||
Guid? ParentCommentId,
|
||||
string Body);
|
||||
string Body,
|
||||
IFormFile? Attachment);
|
||||
|
||||
public class CreateCommentRequestValidator
|
||||
: Validator<CreateCommentRequest>
|
||||
@@ -21,13 +25,15 @@ public class CreateCommentRequestValidator
|
||||
{
|
||||
RuleFor(x => x.WorkspaceId).NotEmpty();
|
||||
RuleFor(x => x.ContentItemId).NotEmpty();
|
||||
RuleFor(x => x.Body).NotEmpty().MaximumLength(4000);
|
||||
RuleFor(x => x.Body).MaximumLength(4000);
|
||||
}
|
||||
}
|
||||
|
||||
public class CreateCommentHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
IBlobStorage blobStorage,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<CreateCommentRequest, CommentDto>
|
||||
{
|
||||
@@ -35,10 +41,19 @@ public class CreateCommentHandler(
|
||||
{
|
||||
Post("/api/comments");
|
||||
Options(o => o.WithTags("Comments"));
|
||||
AllowFileUploads();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CreateCommentRequest request, CancellationToken ct)
|
||||
{
|
||||
string body = request.Body?.Trim() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(body) && request.Attachment is null)
|
||||
{
|
||||
AddError(request => request.Body, "A comment body or attachment is required.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
ContentItem? contentItem = await dbContext.ContentItems
|
||||
.SingleOrDefaultAsync(
|
||||
candidate => candidate.Id == request.ContentItemId && candidate.WorkspaceId == request.WorkspaceId,
|
||||
@@ -72,16 +87,70 @@ public class CreateCommentHandler(
|
||||
}
|
||||
}
|
||||
|
||||
Guid commentId = Guid.NewGuid();
|
||||
string? attachmentFileName = null;
|
||||
string? attachmentContentType = null;
|
||||
long? attachmentSizeBytes = null;
|
||||
string? attachmentBlobName = null;
|
||||
string? attachmentBlobUrl = null;
|
||||
|
||||
if (request.Attachment is not null)
|
||||
{
|
||||
string normalizedContentType = request.Attachment.ContentType.Trim().ToLowerInvariant();
|
||||
|
||||
if (request.Attachment.Length <= 0)
|
||||
{
|
||||
AddError(request => request.Attachment, "The attachment must not be empty.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!IsInlineAttachmentContentType(normalizedContentType))
|
||||
{
|
||||
AddError(request => request.Attachment, "The attachment must be a PNG or JPEG image.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
attachmentFileName = NormalizeFileName(request.Attachment.FileName, normalizedContentType);
|
||||
attachmentContentType = normalizedContentType;
|
||||
attachmentSizeBytes = request.Attachment.Length;
|
||||
attachmentBlobName =
|
||||
$"{contentItem.WorkspaceId}/{SubDirectoryNames.Contents}/{contentItem.Id}/comments/{commentId}/{attachmentFileName}";
|
||||
|
||||
try
|
||||
{
|
||||
attachmentBlobUrl = await blobStorage.UploadFileAsync(
|
||||
ContainerNames.Workspaces,
|
||||
attachmentBlobName,
|
||||
request.Attachment.OpenReadStream(),
|
||||
normalizedContentType,
|
||||
ct);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
AddError(request => request.Attachment, "The attachment file is invalid or unsupported.");
|
||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Comment comment = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Id = commentId,
|
||||
WorkspaceId = request.WorkspaceId,
|
||||
ContentItemId = request.ContentItemId,
|
||||
ParentCommentId = request.ParentCommentId,
|
||||
AuthorUserId = User.GetUserId(),
|
||||
AuthorDisplayName = User.GetAlias() ?? User.GetName(),
|
||||
AuthorEmail = User.GetEmail(),
|
||||
Body = request.Body.Trim(),
|
||||
Body = body,
|
||||
AttachmentFileName = attachmentFileName,
|
||||
AttachmentContentType = attachmentContentType,
|
||||
AttachmentSizeBytes = attachmentSizeBytes,
|
||||
AttachmentBlobContainerName = attachmentBlobName is not null ? ContainerNames.Workspaces : null,
|
||||
AttachmentBlobName = attachmentBlobName,
|
||||
AttachmentBlobUrl = attachmentBlobUrl,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
@@ -93,6 +162,23 @@ public class CreateCommentHandler(
|
||||
.Select(candidate => candidate.PortraitUrl)
|
||||
.SingleOrDefaultAsync(ct);
|
||||
|
||||
await activityWriter.WriteAsync(
|
||||
new ContentItemActivityWriteModel(
|
||||
comment.WorkspaceId,
|
||||
comment.ContentItemId,
|
||||
"comment.created",
|
||||
"Comment",
|
||||
comment.Id,
|
||||
$"{comment.AuthorDisplayName} commented on {contentItem.Title}.",
|
||||
comment.AuthorUserId,
|
||||
comment.AuthorEmail,
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
parentCommentId = comment.ParentCommentId,
|
||||
attachmentFileName = comment.AttachmentFileName,
|
||||
})),
|
||||
ct);
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
comment.WorkspaceId,
|
||||
@@ -116,10 +202,34 @@ public class CreateCommentHandler(
|
||||
comment.AuthorEmail,
|
||||
authorPortraitUrl,
|
||||
comment.Body,
|
||||
comment.IsResolved,
|
||||
comment.CreatedAt,
|
||||
comment.ResolvedAt);
|
||||
comment.AttachmentFileName,
|
||||
comment.AttachmentContentType,
|
||||
comment.AttachmentSizeBytes,
|
||||
comment.AttachmentBlobUrl,
|
||||
comment.CreatedAt);
|
||||
|
||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
||||
}
|
||||
|
||||
private static bool IsInlineAttachmentContentType(string contentType)
|
||||
{
|
||||
return contentType.Trim().ToLowerInvariant() is "image/png" or "image/jpeg" or "image/jpg";
|
||||
}
|
||||
|
||||
private static string NormalizeFileName(string? fileName, string contentType)
|
||||
{
|
||||
string extension = contentType.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"image/png" => ".png",
|
||||
"image/jpeg" or "image/jpg" => ".jpg",
|
||||
_ => string.Empty,
|
||||
};
|
||||
string normalized = Path.GetFileName(fileName ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return $"comment-attachment{extension}";
|
||||
}
|
||||
|
||||
return normalized.Length > 256 ? normalized[..256] : normalized;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@ public record CommentDto(
|
||||
string AuthorEmail,
|
||||
string? AuthorPortraitUrl,
|
||||
string Body,
|
||||
bool IsResolved,
|
||||
DateTimeOffset CreatedAt,
|
||||
DateTimeOffset? ResolvedAt);
|
||||
string? AttachmentFileName,
|
||||
string? AttachmentContentType,
|
||||
long? AttachmentSizeBytes,
|
||||
string? AttachmentBlobUrl,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
public class GetCommentsHandler(
|
||||
AppDbContext dbContext,
|
||||
@@ -75,9 +77,11 @@ public class GetCommentsHandler(
|
||||
comment.AuthorEmail,
|
||||
authorPortraits.GetValueOrDefault(comment.AuthorUserId),
|
||||
comment.Body,
|
||||
comment.IsResolved,
|
||||
comment.CreatedAt,
|
||||
comment.ResolvedAt))
|
||||
comment.AttachmentFileName,
|
||||
comment.AttachmentContentType,
|
||||
comment.AttachmentSizeBytes,
|
||||
comment.AttachmentBlobUrl,
|
||||
comment.CreatedAt))
|
||||
.ToList();
|
||||
|
||||
await SendOkAsync(dtos, ct);
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Comments.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
|
||||
namespace Socialize.Api.Modules.Comments.Handlers;
|
||||
|
||||
public class ResolveCommentHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: EndpointWithoutRequest<CommentDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/comments/{id}/resolve");
|
||||
Options(o => o.WithTags("Comments"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
Guid id = Route<Guid>("id");
|
||||
|
||||
Comment? comment = await dbContext.Comments.SingleOrDefaultAsync(candidate => candidate.Id == id, ct);
|
||||
if (comment is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
ContentItem? contentItem = await dbContext.ContentItems
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == comment.ContentItemId, ct);
|
||||
if (contentItem is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
bool canResolve = await accessScopeService.CanManageWorkspaceAsync(User, comment.WorkspaceId, ct)
|
||||
|| await accessScopeService.CanContributeToCampaignAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct);
|
||||
|
||||
if (!canResolve)
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
comment.IsResolved = true;
|
||||
comment.ResolvedAt = comment.ResolvedAt ?? DateTimeOffset.UtcNow;
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
string? authorPortraitUrl = await dbContext.Users
|
||||
.Where(candidate => candidate.Id == comment.AuthorUserId)
|
||||
.Select(candidate => candidate.PortraitUrl)
|
||||
.SingleOrDefaultAsync(ct);
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
comment.WorkspaceId,
|
||||
comment.ContentItemId,
|
||||
"comment.resolved",
|
||||
"Comment",
|
||||
comment.Id,
|
||||
$"{User.GetAlias() ?? User.GetName()} resolved a comment.",
|
||||
null,
|
||||
null,
|
||||
null),
|
||||
ct);
|
||||
|
||||
CommentDto dto = new(
|
||||
comment.Id,
|
||||
comment.WorkspaceId,
|
||||
comment.ContentItemId,
|
||||
comment.ParentCommentId,
|
||||
comment.AuthorUserId,
|
||||
comment.AuthorDisplayName,
|
||||
comment.AuthorEmail,
|
||||
authorPortraitUrl,
|
||||
comment.Body,
|
||||
comment.IsResolved,
|
||||
comment.CreatedAt,
|
||||
comment.ResolvedAt);
|
||||
|
||||
await SendOkAsync(dto, ct);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Socialize.Api.Modules.ContentItems.Contracts;
|
||||
|
||||
public record ContentItemActivityWriteModel(
|
||||
Guid WorkspaceId,
|
||||
Guid ContentItemId,
|
||||
string EventType,
|
||||
string EntityType,
|
||||
Guid EntityId,
|
||||
string Summary,
|
||||
Guid? ActorUserId,
|
||||
string? ActorEmail,
|
||||
string? MetadataJson);
|
||||
|
||||
public interface IContentItemActivityWriter
|
||||
{
|
||||
Task WriteAsync(ContentItemActivityWriteModel model, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
public class ContentItemActivityEntry
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid WorkspaceId { get; set; }
|
||||
public Guid ContentItemId { get; set; }
|
||||
public required string EventType { get; set; }
|
||||
public required string EntityType { get; set; }
|
||||
public Guid EntityId { get; set; }
|
||||
public required string Summary { get; set; }
|
||||
public Guid? ActorUserId { get; set; }
|
||||
public string? ActorEmail { get; set; }
|
||||
public string? MetadataJson { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
}
|
||||
@@ -41,6 +41,23 @@ public static class ContentItemModelConfiguration
|
||||
revision.HasIndex(x => new { x.ContentItemId, x.RevisionNumber }).IsUnique();
|
||||
});
|
||||
|
||||
modelBuilder.Entity<ContentItemActivityEntry>(entry =>
|
||||
{
|
||||
entry.ToTable("ContentItemActivityEntries");
|
||||
entry.HasKey(x => x.Id);
|
||||
entry.Property(x => x.EventType).HasMaxLength(128).IsRequired();
|
||||
entry.Property(x => x.EntityType).HasMaxLength(128).IsRequired();
|
||||
entry.Property(x => x.Summary).HasMaxLength(1024).IsRequired();
|
||||
entry.Property(x => x.ActorEmail).HasMaxLength(256);
|
||||
entry.Property(x => x.MetadataJson).HasColumnType("jsonb");
|
||||
entry.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
entry.HasIndex(x => x.WorkspaceId);
|
||||
entry.HasIndex(x => x.ContentItemId);
|
||||
entry.HasIndex(x => new { x.ContentItemId, x.CreatedAt });
|
||||
});
|
||||
|
||||
return modelBuilder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems;
|
||||
|
||||
@@ -7,6 +8,8 @@ public static class DependencyInjection
|
||||
public static WebApplicationBuilder AddContentItemsModule(
|
||||
this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddScoped<IContentItemActivityWriter, ContentItemActivityWriter>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
@@ -36,6 +38,7 @@ public class CreateContentItemRequestValidator
|
||||
public class CreateContentItemHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<CreateContentItemRequest, ContentItemDto>
|
||||
{
|
||||
@@ -121,6 +124,26 @@ public class CreateContentItemHandler(
|
||||
});
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await activityWriter.WriteAsync(
|
||||
new ContentItemActivityWriteModel(
|
||||
item.WorkspaceId,
|
||||
item.Id,
|
||||
"content-item.created",
|
||||
"ContentItem",
|
||||
item.Id,
|
||||
$"Content item {item.Title} was created.",
|
||||
User.GetUserId(),
|
||||
User.GetEmail(),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
status = item.Status,
|
||||
revisionLabel = item.CurrentRevisionLabel,
|
||||
dueDate = item.DueDate,
|
||||
publicationTargets = item.PublicationTargets,
|
||||
hashtags = item.Hashtags,
|
||||
})),
|
||||
ct);
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
item.WorkspaceId,
|
||||
|
||||
@@ -2,8 +2,10 @@ using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
@@ -12,7 +14,8 @@ public record CreateContentItemRevisionRequest(
|
||||
string PublicationMessage,
|
||||
string PublicationTargets,
|
||||
string? Hashtags,
|
||||
string? ChangeSummary);
|
||||
string? ChangeSummary,
|
||||
DateTimeOffset? DueDate);
|
||||
|
||||
public class CreateContentItemRevisionRequestValidator
|
||||
: Validator<CreateContentItemRevisionRequest>
|
||||
@@ -30,6 +33,7 @@ public class CreateContentItemRevisionRequestValidator
|
||||
public class CreateContentItemRevisionHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<CreateContentItemRevisionRequest, ContentItemRevisionDto>
|
||||
{
|
||||
@@ -58,11 +62,21 @@ public class CreateContentItemRevisionHandler(
|
||||
|
||||
int revisionNumber = item.CurrentRevisionNumber + 1;
|
||||
string revisionLabel = $"v{revisionNumber}";
|
||||
string previousTitle = item.Title;
|
||||
string previousPublicationMessage = item.PublicationMessage;
|
||||
string previousPublicationTargets = item.PublicationTargets;
|
||||
string? previousHashtags = item.Hashtags;
|
||||
DateTimeOffset? previousDueDate = item.DueDate;
|
||||
string newTitle = request.Title.Trim();
|
||||
string newPublicationMessage = request.PublicationMessage.Trim();
|
||||
string newPublicationTargets = request.PublicationTargets.Trim();
|
||||
string? newHashtags = string.IsNullOrWhiteSpace(request.Hashtags) ? null : request.Hashtags.Trim();
|
||||
|
||||
item.Title = request.Title.Trim();
|
||||
item.PublicationMessage = request.PublicationMessage.Trim();
|
||||
item.PublicationTargets = request.PublicationTargets.Trim();
|
||||
item.Hashtags = string.IsNullOrWhiteSpace(request.Hashtags) ? null : request.Hashtags.Trim();
|
||||
item.Title = newTitle;
|
||||
item.PublicationMessage = newPublicationMessage;
|
||||
item.PublicationTargets = newPublicationTargets;
|
||||
item.Hashtags = newHashtags;
|
||||
item.DueDate = request.DueDate;
|
||||
item.CurrentRevisionNumber = revisionNumber;
|
||||
item.CurrentRevisionLabel = revisionLabel;
|
||||
|
||||
@@ -84,6 +98,32 @@ public class CreateContentItemRevisionHandler(
|
||||
dbContext.ContentItemRevisions.Add(revision);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
List<object> changedFields = [];
|
||||
AddChangedField(changedFields, "title", previousTitle, item.Title);
|
||||
AddChangedField(changedFields, "publicationMessage", previousPublicationMessage, item.PublicationMessage);
|
||||
AddChangedField(changedFields, "publicationTargets", previousPublicationTargets, item.PublicationTargets);
|
||||
AddChangedField(changedFields, "hashtags", previousHashtags, item.Hashtags);
|
||||
AddChangedField(changedFields, "dueDate", previousDueDate, item.DueDate);
|
||||
|
||||
await activityWriter.WriteAsync(
|
||||
new ContentItemActivityWriteModel(
|
||||
item.WorkspaceId,
|
||||
item.Id,
|
||||
"content-item.revision.created",
|
||||
"ContentItemRevision",
|
||||
revision.Id,
|
||||
$"Revision {revisionLabel} was created for {item.Title}.",
|
||||
User.GetUserId(),
|
||||
User.GetEmail(),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
revisionLabel,
|
||||
revisionNumber,
|
||||
changeSummary = revision.ChangeSummary,
|
||||
changedFields,
|
||||
})),
|
||||
ct);
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
item.WorkspaceId,
|
||||
@@ -112,4 +152,19 @@ public class CreateContentItemRevisionHandler(
|
||||
|
||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
||||
}
|
||||
|
||||
private static void AddChangedField<T>(List<object> changedFields, string field, T oldValue, T newValue)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
changedFields.Add(new
|
||||
{
|
||||
field,
|
||||
oldValue,
|
||||
newValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
public record ContentItemActivityEntryDto(
|
||||
Guid Id,
|
||||
Guid WorkspaceId,
|
||||
Guid ContentItemId,
|
||||
string EventType,
|
||||
string EntityType,
|
||||
Guid EntityId,
|
||||
string Summary,
|
||||
Guid? ActorUserId,
|
||||
string? ActorEmail,
|
||||
string? MetadataJson,
|
||||
DateTimeOffset CreatedAt);
|
||||
|
||||
public class GetContentItemActivityHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService)
|
||||
: EndpointWithoutRequest<IReadOnlyCollection<ContentItemActivityEntryDto>>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Get("/api/content-items/{id}/activity");
|
||||
Options(o => o.WithTags("Content Items"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(CancellationToken ct)
|
||||
{
|
||||
Guid id = Route<Guid>("id");
|
||||
|
||||
ContentItem? item = await dbContext.ContentItems
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == id, ct);
|
||||
|
||||
if (item is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
List<ContentItemActivityEntryDto> entries = await dbContext.ContentItemActivityEntries
|
||||
.Where(entry => entry.ContentItemId == item.Id)
|
||||
.OrderByDescending(entry => entry.CreatedAt)
|
||||
.Take(200)
|
||||
.Select(entry => new ContentItemActivityEntryDto(
|
||||
entry.Id,
|
||||
entry.WorkspaceId,
|
||||
entry.ContentItemId,
|
||||
entry.EventType,
|
||||
entry.EntityType,
|
||||
entry.EntityId,
|
||||
entry.Summary,
|
||||
entry.ActorUserId,
|
||||
entry.ActorEmail,
|
||||
entry.MetadataJson,
|
||||
entry.CreatedAt))
|
||||
.ToListAsync(ct);
|
||||
|
||||
await SendOkAsync(entries, ct);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@ using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.Security;
|
||||
using Socialize.Api.Modules.Approvals.Services;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
using Socialize.Api.Modules.Notifications.Contracts;
|
||||
using Socialize.Api.Modules.Workspaces.Data;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||
|
||||
@@ -24,6 +26,7 @@ public class UpdateContentItemStatusHandler(
|
||||
AppDbContext dbContext,
|
||||
AccessScopeService accessScopeService,
|
||||
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||
IContentItemActivityWriter activityWriter,
|
||||
INotificationEventWriter notificationEventWriter)
|
||||
: Endpoint<UpdateContentItemStatusRequest, ContentItemDetailDto>
|
||||
{
|
||||
@@ -122,12 +125,33 @@ public class UpdateContentItemStatusHandler(
|
||||
}
|
||||
}
|
||||
|
||||
string previousStatus = item.Status;
|
||||
if (item.Status != "In approval" || normalizedStatus != "In approval")
|
||||
{
|
||||
item.Status = normalizedStatus;
|
||||
}
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
if (previousStatus != item.Status)
|
||||
{
|
||||
await activityWriter.WriteAsync(
|
||||
new ContentItemActivityWriteModel(
|
||||
item.WorkspaceId,
|
||||
item.Id,
|
||||
"content-item.status.updated",
|
||||
"ContentItem",
|
||||
item.Id,
|
||||
$"Status changed from {previousStatus} to {item.Status} for {item.Title}.",
|
||||
User.GetUserId(),
|
||||
User.GetEmail(),
|
||||
JsonSerializer.Serialize(new
|
||||
{
|
||||
oldValue = previousStatus,
|
||||
newValue = item.Status,
|
||||
})),
|
||||
ct);
|
||||
}
|
||||
|
||||
await notificationEventWriter.WriteAsync(
|
||||
new NotificationEventWriteModel(
|
||||
item.WorkspaceId,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||
using Socialize.Api.Modules.ContentItems.Data;
|
||||
|
||||
namespace Socialize.Api.Modules.ContentItems.Services;
|
||||
|
||||
public class ContentItemActivityWriter(
|
||||
AppDbContext dbContext)
|
||||
: IContentItemActivityWriter
|
||||
{
|
||||
public async Task WriteAsync(ContentItemActivityWriteModel model, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ContentItemActivityEntry entry = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
WorkspaceId = model.WorkspaceId,
|
||||
ContentItemId = model.ContentItemId,
|
||||
EventType = model.EventType,
|
||||
EntityType = model.EntityType,
|
||||
EntityId = model.EntityId,
|
||||
Summary = model.Summary,
|
||||
ActorUserId = model.ActorUserId,
|
||||
ActorEmail = model.ActorEmail,
|
||||
MetadataJson = model.MetadataJson,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
dbContext.ContentItemActivityEntries.Add(entry);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ public class Organization
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public required string Name { get; set; }
|
||||
public string? LogoUrl { get; set; }
|
||||
public Guid OwnerUserId { get; set; }
|
||||
public DateTimeOffset CreatedAt { get; init; }
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public static class OrganizationModelConfiguration
|
||||
organization.ToTable("Organizations");
|
||||
organization.HasKey(x => x.Id);
|
||||
organization.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||
organization.Property(x => x.LogoUrl).HasMaxLength(2048);
|
||||
organization.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Modules.Identity.Data;
|
||||
using Socialize.Api.Modules.Organizations.Data;
|
||||
using Socialize.Api.Modules.Organizations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.Organizations.Handlers;
|
||||
|
||||
public record AddOrganizationMemberRequest(
|
||||
string Email,
|
||||
string Role);
|
||||
|
||||
public class AddOrganizationMemberRequestValidator
|
||||
: Validator<AddOrganizationMemberRequest>
|
||||
{
|
||||
private static readonly string[] AllowedRoles =
|
||||
[
|
||||
OrganizationRoles.Admin,
|
||||
OrganizationRoles.BillingManager,
|
||||
OrganizationRoles.ConnectorManager,
|
||||
OrganizationRoles.Member,
|
||||
];
|
||||
|
||||
public AddOrganizationMemberRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Email).NotEmpty().MaximumLength(256).EmailAddress();
|
||||
RuleFor(x => x.Role)
|
||||
.NotEmpty()
|
||||
.Must(role => AllowedRoles.Contains(role.Trim(), StringComparer.Ordinal))
|
||||
.WithMessage("A valid organization role should be specified.");
|
||||
}
|
||||
}
|
||||
|
||||
public class AddOrganizationMemberHandler(
|
||||
AppDbContext dbContext,
|
||||
OrganizationAccessService organizationAccessService)
|
||||
: Endpoint<AddOrganizationMemberRequest, OrganizationMemberDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/organizations/{organizationId:guid}/members");
|
||||
Options(o => o.WithTags("Organizations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(AddOrganizationMemberRequest request, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Guid organizationId = Route<Guid>("organizationId");
|
||||
|
||||
if (!await dbContext.Organizations.AnyAsync(organization => organization.Id == organizationId, ct))
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await organizationAccessService.HasOrganizationPermissionAsync(
|
||||
User,
|
||||
organizationId,
|
||||
OrganizationPermissions.ManageOrganizationMembers,
|
||||
ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
string normalizedEmail = request.Email.Trim().ToUpperInvariant();
|
||||
User? user = await dbContext.Users
|
||||
.SingleOrDefaultAsync(candidate => candidate.NormalizedEmail == normalizedEmail, ct);
|
||||
if (user is null)
|
||||
{
|
||||
AddError(request => request.Email, "No user account exists for this email address.");
|
||||
await SendErrorsAsync(StatusCodes.Status404NotFound, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
bool duplicateMembership = await dbContext.OrganizationMemberships.AnyAsync(
|
||||
membership => membership.OrganizationId == organizationId && membership.UserId == user.Id,
|
||||
ct);
|
||||
if (duplicateMembership)
|
||||
{
|
||||
AddError(request => request.Email, "This user is already a member of the organization.");
|
||||
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
string role = request.Role.Trim();
|
||||
OrganizationMembership membership = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = organizationId,
|
||||
UserId = user.Id,
|
||||
Role = role,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
|
||||
dbContext.OrganizationMemberships.Add(membership);
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendAsync(
|
||||
new OrganizationMemberDto(
|
||||
user.Id,
|
||||
BuildDisplayName(user),
|
||||
user.Email ?? string.Empty,
|
||||
user.PortraitUrl,
|
||||
membership.Role,
|
||||
OrganizationPermissionRules.GetPermissionsForRole(membership.Role),
|
||||
membership.CreatedAt),
|
||||
StatusCodes.Status201Created,
|
||||
ct);
|
||||
}
|
||||
|
||||
private static string BuildDisplayName(User user)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(user.Alias))
|
||||
{
|
||||
return user.Alias;
|
||||
}
|
||||
|
||||
string fullName = $"{user.Firstname} {user.Lastname}".Trim();
|
||||
if (!string.IsNullOrWhiteSpace(fullName))
|
||||
{
|
||||
return fullName;
|
||||
}
|
||||
|
||||
return user.Email ?? user.UserName ?? user.Id.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||
using Socialize.Api.Modules.Organizations.Data;
|
||||
using Socialize.Api.Modules.Organizations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.Organizations.Handlers;
|
||||
|
||||
public record ChangeOrganizationLogoRequest(
|
||||
IFormFile File);
|
||||
|
||||
public record ChangeOrganizationLogoResponse(
|
||||
string BlobUrl);
|
||||
|
||||
public sealed class ChangeOrganizationLogoRequestValidator : Validator<ChangeOrganizationLogoRequest>
|
||||
{
|
||||
public ChangeOrganizationLogoRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.File)
|
||||
.NotNull()
|
||||
.NotEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public class ChangeOrganizationLogoHandler(
|
||||
AppDbContext dbContext,
|
||||
IBlobStorage blobStorage,
|
||||
OrganizationAccessService organizationAccessService)
|
||||
: Endpoint<ChangeOrganizationLogoRequest, ChangeOrganizationLogoResponse>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Post("/api/organizations/{organizationId:guid}/logo");
|
||||
Options(o => o.WithTags("Organizations"));
|
||||
AllowFileUploads();
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(ChangeOrganizationLogoRequest request, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Guid organizationId = Route<Guid>("organizationId");
|
||||
|
||||
Organization? organization = await dbContext.Organizations
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == organizationId, ct);
|
||||
if (organization is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await organizationAccessService.HasOrganizationPermissionAsync(
|
||||
User,
|
||||
organizationId,
|
||||
OrganizationPermissions.ManageOrganizationSettings,
|
||||
ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
string blobUrl = await blobStorage.UploadFileAsync(
|
||||
ContainerNames.Organizations,
|
||||
$"{organization.Id}/{SubDirectoryNames.Profile}/{CommonFileNames.LogoPicture}",
|
||||
request.File.OpenReadStream(),
|
||||
request.File.ContentType,
|
||||
ct);
|
||||
|
||||
organization.LogoUrl = blobUrl;
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
await SendOkAsync(new ChangeOrganizationLogoResponse(blobUrl), ct);
|
||||
}
|
||||
}
|
||||
@@ -44,13 +44,15 @@ public class GetOrganizationHandler(
|
||||
|
||||
IReadOnlyCollection<OrganizationMemberDto> members = await GetMembersAsync(organizationId, ct);
|
||||
IReadOnlyCollection<WorkspaceDto> workspaces = await GetWorkspacesAsync(organizationId, ct);
|
||||
OrganizationUsageDto usage = await GetUsageAsync(organization, ct);
|
||||
|
||||
await SendOkAsync(
|
||||
OrganizationDto.FromOrganization(
|
||||
organization,
|
||||
currentUserPermissions,
|
||||
members,
|
||||
workspaces),
|
||||
workspaces,
|
||||
usage),
|
||||
ct);
|
||||
}
|
||||
|
||||
@@ -96,6 +98,57 @@ public class GetOrganizationHandler(
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private async Task<OrganizationUsageDto> GetUsageAsync(
|
||||
Organization organization,
|
||||
CancellationToken ct)
|
||||
{
|
||||
Guid[] workspaceIds = await dbContext.Workspaces
|
||||
.Where(workspace => workspace.OrganizationId == organization.Id)
|
||||
.Select(workspace => workspace.Id)
|
||||
.ToArrayAsync(ct);
|
||||
|
||||
Guid[] memberUserIds = await dbContext.OrganizationMemberships
|
||||
.Where(membership => membership.OrganizationId == organization.Id)
|
||||
.Select(membership => membership.UserId)
|
||||
.Distinct()
|
||||
.ToArrayAsync(ct);
|
||||
int userCount = memberUserIds
|
||||
.Append(organization.OwnerUserId)
|
||||
.Distinct()
|
||||
.Count();
|
||||
|
||||
int activeContentItemCount = workspaceIds.Length == 0
|
||||
? 0
|
||||
: await dbContext.ContentItems
|
||||
.Where(contentItem => workspaceIds.Contains(contentItem.WorkspaceId) &&
|
||||
contentItem.Status != "Approved" &&
|
||||
contentItem.Status != "Scheduled")
|
||||
.CountAsync(ct);
|
||||
|
||||
OrganizationUsageLimits limits = GetUsageLimits(organization.Name);
|
||||
|
||||
return new OrganizationUsageDto(
|
||||
limits.PlanName,
|
||||
[
|
||||
new OrganizationUsageItemDto("users", userCount, limits.UserLimit),
|
||||
new OrganizationUsageItemDto("workspaces", workspaceIds.Length, limits.WorkspaceLimit),
|
||||
new OrganizationUsageItemDto("activeContent", activeContentItemCount, limits.ActiveContentLimit),
|
||||
]);
|
||||
}
|
||||
|
||||
private static OrganizationUsageLimits GetUsageLimits(string organizationName)
|
||||
{
|
||||
return string.Equals(organizationName, "Northstar Agency", StringComparison.OrdinalIgnoreCase)
|
||||
? new OrganizationUsageLimits("Agency", 25, 15, 250)
|
||||
: new OrganizationUsageLimits("Free", 2, 1, 3);
|
||||
}
|
||||
|
||||
private sealed record OrganizationUsageLimits(
|
||||
string PlanName,
|
||||
int UserLimit,
|
||||
int WorkspaceLimit,
|
||||
int ActiveContentLimit);
|
||||
|
||||
private static string BuildDisplayName(User user)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(user.Alias))
|
||||
|
||||
@@ -15,25 +15,39 @@ public record OrganizationMemberDto(
|
||||
public record OrganizationDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
string? LogoUrl,
|
||||
Guid OwnerUserId,
|
||||
IReadOnlyCollection<string> CurrentUserPermissions,
|
||||
IReadOnlyCollection<OrganizationMemberDto> Members,
|
||||
IReadOnlyCollection<WorkspaceDto> Workspaces,
|
||||
OrganizationUsageDto? Usage,
|
||||
DateTimeOffset CreatedAt)
|
||||
{
|
||||
public static OrganizationDto FromOrganization(
|
||||
Organization organization,
|
||||
IReadOnlyCollection<string> currentUserPermissions,
|
||||
IReadOnlyCollection<OrganizationMemberDto>? members = null,
|
||||
IReadOnlyCollection<WorkspaceDto>? workspaces = null)
|
||||
IReadOnlyCollection<WorkspaceDto>? workspaces = null,
|
||||
OrganizationUsageDto? usage = null)
|
||||
{
|
||||
return new OrganizationDto(
|
||||
organization.Id,
|
||||
organization.Name,
|
||||
organization.LogoUrl,
|
||||
organization.OwnerUserId,
|
||||
currentUserPermissions,
|
||||
members ?? [],
|
||||
workspaces ?? [],
|
||||
usage,
|
||||
organization.CreatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
public record OrganizationUsageDto(
|
||||
string PlanName,
|
||||
IReadOnlyCollection<OrganizationUsageItemDto> Items);
|
||||
|
||||
public record OrganizationUsageItemDto(
|
||||
string Key,
|
||||
int Used,
|
||||
int? Limit);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using FastEndpoints;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Socialize.Api.Data;
|
||||
using Socialize.Api.Modules.Organizations.Data;
|
||||
using Socialize.Api.Modules.Organizations.Services;
|
||||
|
||||
namespace Socialize.Api.Modules.Organizations.Handlers;
|
||||
|
||||
public record UpdateOrganizationRequest(
|
||||
string Name);
|
||||
|
||||
public class UpdateOrganizationRequestValidator
|
||||
: Validator<UpdateOrganizationRequest>
|
||||
{
|
||||
public UpdateOrganizationRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||
}
|
||||
}
|
||||
|
||||
public class UpdateOrganizationHandler(
|
||||
AppDbContext dbContext,
|
||||
OrganizationAccessService organizationAccessService)
|
||||
: Endpoint<UpdateOrganizationRequest, OrganizationDto>
|
||||
{
|
||||
public override void Configure()
|
||||
{
|
||||
Put("/api/organizations/{organizationId:guid}");
|
||||
Options(o => o.WithTags("Organizations"));
|
||||
}
|
||||
|
||||
public override async Task HandleAsync(UpdateOrganizationRequest request, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
Guid organizationId = Route<Guid>("organizationId");
|
||||
|
||||
Organization? organization = await dbContext.Organizations
|
||||
.SingleOrDefaultAsync(candidate => candidate.Id == organizationId, ct);
|
||||
if (organization is null)
|
||||
{
|
||||
await SendNotFoundAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!await organizationAccessService.HasOrganizationPermissionAsync(
|
||||
User,
|
||||
organizationId,
|
||||
OrganizationPermissions.ManageOrganizationSettings,
|
||||
ct))
|
||||
{
|
||||
await SendForbiddenAsync(ct);
|
||||
return;
|
||||
}
|
||||
|
||||
organization.Name = request.Name.Trim();
|
||||
await dbContext.SaveChangesAsync(ct);
|
||||
|
||||
IReadOnlyCollection<string> currentUserPermissions = await organizationAccessService.GetUserOrganizationPermissionsAsync(
|
||||
User,
|
||||
organizationId,
|
||||
ct);
|
||||
|
||||
await SendOkAsync(OrganizationDto.FromOrganization(organization, currentUserPermissions), ct);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ public class Workspace
|
||||
{
|
||||
public Guid Id { get; init; }
|
||||
public required string Name { get; set; }
|
||||
public required string Slug { get; set; }
|
||||
public string? LogoUrl { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid OwnerUserId { get; set; }
|
||||
|
||||
@@ -12,7 +12,6 @@ public static class WorkspaceModelConfiguration
|
||||
workspace.ToTable("Workspaces");
|
||||
workspace.HasKey(x => x.Id);
|
||||
workspace.Property(x => x.Name).HasMaxLength(256).IsRequired();
|
||||
workspace.Property(x => x.Slug).HasMaxLength(128).IsRequired();
|
||||
workspace.Property(x => x.LogoUrl).HasMaxLength(2048);
|
||||
workspace.Property(x => x.TimeZone).HasMaxLength(128).IsRequired();
|
||||
workspace.Property(x => x.ApprovalMode).HasMaxLength(32).IsRequired().HasDefaultValue("Required");
|
||||
@@ -22,7 +21,6 @@ public static class WorkspaceModelConfiguration
|
||||
workspace.Property(x => x.CreatedAt)
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
workspace.HasIndex(x => x.Slug).IsUnique();
|
||||
workspace.HasIndex(x => x.OrganizationId);
|
||||
workspace.HasIndex(x => x.OwnerUserId);
|
||||
workspace.HasOne<Organization>()
|
||||
|
||||
@@ -9,7 +9,6 @@ namespace Socialize.Api.Modules.Workspaces.Handlers;
|
||||
public record CreateWorkspaceRequest(
|
||||
Guid OrganizationId,
|
||||
string Name,
|
||||
string Slug,
|
||||
string TimeZone);
|
||||
|
||||
public class CreateWorkspaceRequestValidator
|
||||
@@ -19,10 +18,6 @@ public class CreateWorkspaceRequestValidator
|
||||
{
|
||||
RuleFor(x => x.OrganizationId).NotEmpty();
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(256);
|
||||
RuleFor(x => x.Slug)
|
||||
.NotEmpty()
|
||||
.MaximumLength(128)
|
||||
.Matches("^[a-z0-9]+(?:-[a-z0-9]+)*$");
|
||||
RuleFor(x => x.TimeZone).NotEmpty().MaximumLength(128);
|
||||
}
|
||||
}
|
||||
@@ -56,25 +51,13 @@ public class CreateWorkspaceHandler(
|
||||
}
|
||||
|
||||
string normalizedName = request.Name.Trim();
|
||||
string normalizedSlug = request.Slug.Trim().ToLowerInvariant();
|
||||
string normalizedTimeZone = request.TimeZone.Trim();
|
||||
|
||||
bool duplicateWorkspace = await dbContext.Workspaces
|
||||
.AnyAsync(workspace => workspace.Slug == normalizedSlug, ct);
|
||||
|
||||
if (duplicateWorkspace)
|
||||
{
|
||||
AddError(request => request.Slug, "A workspace with this slug already exists.");
|
||||
await SendErrorsAsync(StatusCodes.Status409Conflict, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
Workspace workspace = new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OrganizationId = request.OrganizationId,
|
||||
Name = normalizedName,
|
||||
Slug = normalizedSlug,
|
||||
OwnerUserId = User.GetUserId(),
|
||||
TimeZone = normalizedTimeZone,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
|
||||
@@ -21,7 +21,6 @@ public record WorkspaceDto(
|
||||
Guid Id,
|
||||
Guid OrganizationId,
|
||||
string Name,
|
||||
string Slug,
|
||||
string? LogoUrl,
|
||||
string TimeZone,
|
||||
string ApprovalMode,
|
||||
@@ -39,7 +38,6 @@ public record WorkspaceDto(
|
||||
workspace.Id,
|
||||
workspace.OrganizationId,
|
||||
workspace.Name,
|
||||
workspace.Slug,
|
||||
workspace.LogoUrl,
|
||||
workspace.TimeZone,
|
||||
workspace.ApprovalMode,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Azure.Identity;
|
||||
using FastEndpoints;
|
||||
using FastEndpoints.Swagger;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
@@ -10,6 +9,7 @@ using Socialize.Api.Infrastructure;
|
||||
using Socialize.Api.Infrastructure.Development;
|
||||
using Socialize.Api.Modules.Approvals;
|
||||
using Socialize.Api.Modules.Assets;
|
||||
using Socialize.Api.Modules.Channels;
|
||||
using Socialize.Api.Modules.Clients;
|
||||
using Socialize.Api.Modules.Comments;
|
||||
using Socialize.Api.Modules.ContentItems;
|
||||
@@ -17,17 +17,17 @@ using Socialize.Api.Modules.Feedback;
|
||||
using Socialize.Api.Modules.Identity;
|
||||
using Socialize.Api.Modules.Notifications;
|
||||
using Socialize.Api.Modules.Campaigns;
|
||||
using Socialize.Api.Modules.CalendarIntegrations;
|
||||
using Socialize.Api.Modules.Organizations;
|
||||
using Socialize.Api.Modules.Workspaces;
|
||||
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
string? vaultUri = Environment.GetEnvironmentVariable("VaultUri");
|
||||
if (!string.IsNullOrWhiteSpace(vaultUri))
|
||||
{
|
||||
var vaultUri = Environment.GetEnvironmentVariable("VaultUri") ?? throw new InvalidOperationException("Missing VaultUri configuration setting");
|
||||
|
||||
builder.Configuration.AddAzureKeyVault(new Uri(vaultUri), new DefaultAzureCredential());
|
||||
throw new InvalidOperationException("VaultUri configuration is not supported by this deployment. Move secrets to environment variables.");
|
||||
}
|
||||
|
||||
builder.Services.AddCors(options =>
|
||||
@@ -65,6 +65,7 @@ builder.AddInfrastructureModule();
|
||||
builder.AddIdentityModule();
|
||||
builder.AddOrganizationsModule();
|
||||
builder.AddWorkspaceModule();
|
||||
builder.AddChannelsModule();
|
||||
builder.AddClientsModule();
|
||||
builder.AddCampaignsModule();
|
||||
builder.AddContentItemsModule();
|
||||
@@ -73,6 +74,7 @@ builder.AddCommentsModule();
|
||||
builder.AddApprovalsModule();
|
||||
builder.AddNotificationsModule();
|
||||
builder.AddFeedbackModule();
|
||||
builder.AddCalendarIntegrationsModule();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
@@ -11,12 +11,11 @@
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<WarningsAsErrors />
|
||||
<NoWarn>CA2007</NoWarn> <!-- disable ConfigureAwait warning - not present in ASP.NET Core -->
|
||||
<NoWarn>$(NoWarn);CA1515;CA2007</NoWarn> <!-- disable ConfigureAwait warning - not present in ASP.NET Core -->
|
||||
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.5.0" />
|
||||
<PackageReference Include="Azure.Identity" Version="1.18.0" />
|
||||
<PackageReference Include="FastEndpoints" Version="5.35.0" />
|
||||
<PackageReference Include="FastEndpoints.Swagger" Version="5.35.0" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"PostgresConnection": "Server=hutopypostgress.postgres.database.azure.com,5432;Database=hutopy;User Id=hutopy;Password=General2024!;Ssl Mode=Require;"
|
||||
},
|
||||
"Stripe": {
|
||||
"SocializeRate": 0.05
|
||||
},
|
||||
"Website": {
|
||||
"FrontendBaseUrl": "https://hutopy.com"
|
||||
"FrontendBaseUrl": "https://socialize.mapachotes.com"
|
||||
},
|
||||
"LocalBlobStorage": {
|
||||
"RootPath": "App_Data/blob-storage",
|
||||
"RequestPath": "/api/storage"
|
||||
},
|
||||
"DevelopmentSeed": {
|
||||
"Enabled": false
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,60 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2"/>
|
||||
</startup>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Build" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Build.Framework" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Build.Utilities.Core" publicKeyToken="b03f5f7f11d50a3a"
|
||||
culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.Build.Tasks.Core" publicKeyToken="b03f5f7f11d50a3a"
|
||||
culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.IO.Redist" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.1" newVersion="6.0.0.1"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a"
|
||||
culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a"
|
||||
culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user