Compare commits
37 Commits
2d472892d6
...
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 | |||
| 664eb07201 | |||
| 58c1301054 | |||
| 552f4f1f21 | |||
| 8f4b95f311 | |||
| 4fba72e99c | |||
| 55d8acef4c | |||
| 7d3f495472 | |||
| 802668fb0b | |||
| cd6f402d9e | |||
| 9bdef978bd |
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
|
# .NET
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
|
**/[Bb]in/
|
||||||
|
**/[Oo]bj/
|
||||||
|
**/[Bb]in[\\]*
|
||||||
|
**/[Oo]bj[\\]*
|
||||||
TestResults/
|
TestResults/
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ Update OpenAPI:
|
|||||||
## Current Domain Modules
|
## Current Domain Modules
|
||||||
|
|
||||||
- `Identity`: authentication, refresh tokens, email verification, password reset, social login.
|
- `Identity`: authentication, refresh tokens, email verification, password reset, social login.
|
||||||
|
- `Organizations`: SaaS account ownership, billing/subscription boundary, organization membership, connectors, data mappings, and owned workspaces.
|
||||||
- `Workspaces`: workspace membership, workspace settings, access scoping.
|
- `Workspaces`: workspace membership, workspace settings, access scoping.
|
||||||
- `Clients`: client records and primary contacts tied to workspaces.
|
- `Clients`: client records and primary contacts tied to workspaces.
|
||||||
- `Projects`: project pipeline and client/project relationships.
|
- `Projects`: project pipeline and client/project relationships.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Socialize
|
# Socialize
|
||||||
|
|
||||||
Socialize is a workspace-based workflow application for social media content review, revision, approval, and publication readiness.
|
Socialize is an organization-owned, workspace-based workflow application for social media content review, revision, approval, and publication readiness.
|
||||||
|
|
||||||
It is not a public social network. The product is for internal teams, providers, and client approvers coordinating content work before publication.
|
It is not a public social network. The product is for internal teams, providers, and client approvers coordinating content work before publication.
|
||||||
|
|
||||||
|
|||||||
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 Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Socialize.Api.Modules.Approvals.Data;
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
using Socialize.Api.Modules.Assets.Data;
|
using Socialize.Api.Modules.Assets.Data;
|
||||||
|
using Socialize.Api.Modules.Channels.Data;
|
||||||
using Socialize.Api.Modules.Clients.Data;
|
using Socialize.Api.Modules.Clients.Data;
|
||||||
using Socialize.Api.Modules.Comments.Data;
|
using Socialize.Api.Modules.Comments.Data;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
@@ -9,6 +10,8 @@ using Socialize.Api.Modules.Feedback.Data;
|
|||||||
using Socialize.Api.Modules.Identity.Data;
|
using Socialize.Api.Modules.Identity.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Data;
|
using Socialize.Api.Modules.Notifications.Data;
|
||||||
using Socialize.Api.Modules.Campaigns.Data;
|
using Socialize.Api.Modules.Campaigns.Data;
|
||||||
|
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Data;
|
namespace Socialize.Api.Data;
|
||||||
@@ -17,12 +20,16 @@ public class AppDbContext(
|
|||||||
DbContextOptions<AppDbContext> options)
|
DbContextOptions<AppDbContext> options)
|
||||||
: IdentityDbContext<User, Role, Guid>(options)
|
: IdentityDbContext<User, Role, Guid>(options)
|
||||||
{
|
{
|
||||||
|
public DbSet<Organization> Organizations => Set<Organization>();
|
||||||
|
public DbSet<OrganizationMembership> OrganizationMemberships => Set<OrganizationMembership>();
|
||||||
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
public DbSet<Workspace> Workspaces => Set<Workspace>();
|
||||||
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
public DbSet<WorkspaceInvite> WorkspaceInvites => Set<WorkspaceInvite>();
|
||||||
|
public DbSet<Channel> Channels => Set<Channel>();
|
||||||
public DbSet<Client> Clients => Set<Client>();
|
public DbSet<Client> Clients => Set<Client>();
|
||||||
public DbSet<Campaign> Campaigns => Set<Campaign>();
|
public DbSet<Campaign> Campaigns => Set<Campaign>();
|
||||||
public DbSet<ContentItem> ContentItems => Set<ContentItem>();
|
public DbSet<ContentItem> ContentItems => Set<ContentItem>();
|
||||||
public DbSet<ContentItemRevision> ContentItemRevisions => Set<ContentItemRevision>();
|
public DbSet<ContentItemRevision> ContentItemRevisions => Set<ContentItemRevision>();
|
||||||
|
public DbSet<ContentItemActivityEntry> ContentItemActivityEntries => Set<ContentItemActivityEntry>();
|
||||||
public DbSet<Asset> Assets => Set<Asset>();
|
public DbSet<Asset> Assets => Set<Asset>();
|
||||||
public DbSet<AssetRevision> AssetRevisions => Set<AssetRevision>();
|
public DbSet<AssetRevision> AssetRevisions => Set<AssetRevision>();
|
||||||
public DbSet<Comment> Comments => Set<Comment>();
|
public DbSet<Comment> Comments => Set<Comment>();
|
||||||
@@ -36,12 +43,18 @@ public class AppDbContext(
|
|||||||
public DbSet<FeedbackScreenshot> FeedbackScreenshots => Set<FeedbackScreenshot>();
|
public DbSet<FeedbackScreenshot> FeedbackScreenshots => Set<FeedbackScreenshot>();
|
||||||
public DbSet<FeedbackComment> FeedbackComments => Set<FeedbackComment>();
|
public DbSet<FeedbackComment> FeedbackComments => Set<FeedbackComment>();
|
||||||
public DbSet<FeedbackActivityEntry> FeedbackActivityEntries => Set<FeedbackActivityEntry>();
|
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)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
|
builder.ConfigureOrganizationsModule();
|
||||||
builder.ConfigureWorkspacesModule();
|
builder.ConfigureWorkspacesModule();
|
||||||
|
builder.ConfigureChannelsModule();
|
||||||
builder.ConfigureClientsModule();
|
builder.ConfigureClientsModule();
|
||||||
builder.ConfigureCampaignsModule();
|
builder.ConfigureCampaignsModule();
|
||||||
builder.ConfigureContentItemsModule();
|
builder.ConfigureContentItemsModule();
|
||||||
@@ -50,5 +63,6 @@ public class AppDbContext(
|
|||||||
builder.ConfigureApprovalsModule();
|
builder.ConfigureApprovalsModule();
|
||||||
builder.ConfigureNotificationsModule();
|
builder.ConfigureNotificationsModule();
|
||||||
builder.ConfigureFeedbackModule();
|
builder.ConfigureFeedbackModule();
|
||||||
|
builder.ConfigureCalendarIntegrationsModule();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ internal static class ContainerNames
|
|||||||
{
|
{
|
||||||
public const string Users = "users";
|
public const string Users = "users";
|
||||||
public const string Clients = "clients";
|
public const string Clients = "clients";
|
||||||
|
public const string Organizations = "organizations";
|
||||||
public const string Workspaces = "workspaces";
|
public const string Workspaces = "workspaces";
|
||||||
public const string Creators = "creators";
|
public const string Creators = "creators";
|
||||||
public const string Feedback = "feedback";
|
public const string Feedback = "feedback";
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ using Socialize.Api.Modules.Identity.Contracts;
|
|||||||
using Socialize.Api.Modules.Identity.Data;
|
using Socialize.Api.Modules.Identity.Data;
|
||||||
using Socialize.Api.Modules.Assets.Data;
|
using Socialize.Api.Modules.Assets.Data;
|
||||||
using Socialize.Api.Modules.Approvals.Data;
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
|
using Socialize.Api.Modules.Channels.Data;
|
||||||
using Socialize.Api.Modules.Comments.Data;
|
using Socialize.Api.Modules.Comments.Data;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Clients.Data;
|
using Socialize.Api.Modules.Clients.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Data;
|
using Socialize.Api.Modules.Notifications.Data;
|
||||||
using Socialize.Api.Modules.Campaigns.Data;
|
using Socialize.Api.Modules.Campaigns.Data;
|
||||||
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
|
using Socialize.Api.Modules.Organizations.Services;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
@@ -19,11 +22,16 @@ namespace Socialize.Api.Infrastructure.Development;
|
|||||||
|
|
||||||
public static class DevelopmentSeedExtensions
|
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 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 ScopedClientId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
||||||
private static readonly Guid HiddenClientId = Guid.Parse("22222222-2222-2222-2222-333333333333");
|
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 ScopedCampaignId = Guid.Parse("33333333-3333-3333-3333-333333333333");
|
||||||
private static readonly Guid HiddenCampaignId = Guid.Parse("33333333-3333-3333-3333-444444444444");
|
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 ScopedContentItemId = Guid.Parse("44444444-4444-4444-4444-444444444444");
|
||||||
private static readonly Guid HiddenContentItemId = Guid.Parse("44444444-4444-4444-4444-555555555555");
|
private static readonly Guid HiddenContentItemId = Guid.Parse("44444444-4444-4444-4444-555555555555");
|
||||||
private static readonly Guid ScopedAssetId = Guid.Parse("55555555-5555-5555-5555-555555555555");
|
private static readonly Guid ScopedAssetId = Guid.Parse("55555555-5555-5555-5555-555555555555");
|
||||||
@@ -117,6 +125,12 @@ public static class DevelopmentSeedExtensions
|
|||||||
[
|
[
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await EnsureOrganizationDataAsync(
|
||||||
|
manager.Id,
|
||||||
|
dev.Id,
|
||||||
|
dbContext,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
await EnsureWorkspaceDataAsync(
|
await EnsureWorkspaceDataAsync(
|
||||||
manager.Id,
|
manager.Id,
|
||||||
clientUser.Id,
|
clientUser.Id,
|
||||||
@@ -224,6 +238,75 @@ public static class DevelopmentSeedExtensions
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task EnsureOrganizationDataAsync(
|
||||||
|
Guid managerUserId,
|
||||||
|
Guid developerUserId,
|
||||||
|
AppDbContext dbContext,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Organization? organization = await dbContext.Organizations
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == OrganizationId, cancellationToken);
|
||||||
|
if (organization is null)
|
||||||
|
{
|
||||||
|
organization = new Organization
|
||||||
|
{
|
||||||
|
Id = OrganizationId,
|
||||||
|
Name = string.Empty,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
};
|
||||||
|
dbContext.Organizations.Add(organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
organization.Name = "Northstar Agency";
|
||||||
|
organization.OwnerUserId = managerUserId;
|
||||||
|
|
||||||
|
await UpsertOrganizationMembershipAsync(
|
||||||
|
dbContext,
|
||||||
|
Guid.Parse("99999999-9999-9999-9999-000000000001"),
|
||||||
|
OrganizationId,
|
||||||
|
managerUserId,
|
||||||
|
OrganizationRoles.Owner,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await UpsertOrganizationMembershipAsync(
|
||||||
|
dbContext,
|
||||||
|
Guid.Parse("99999999-9999-9999-9999-000000000002"),
|
||||||
|
OrganizationId,
|
||||||
|
developerUserId,
|
||||||
|
OrganizationRoles.Admin,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task UpsertOrganizationMembershipAsync(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
Guid membershipId,
|
||||||
|
Guid organizationId,
|
||||||
|
Guid userId,
|
||||||
|
string role,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
OrganizationMembership? membership = await dbContext.OrganizationMemberships
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
candidate => candidate.OrganizationId == organizationId && candidate.UserId == userId,
|
||||||
|
cancellationToken);
|
||||||
|
if (membership is null)
|
||||||
|
{
|
||||||
|
membership = new OrganizationMembership
|
||||||
|
{
|
||||||
|
Id = membershipId,
|
||||||
|
OrganizationId = organizationId,
|
||||||
|
UserId = userId,
|
||||||
|
Role = role,
|
||||||
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
|
};
|
||||||
|
dbContext.OrganizationMemberships.Add(membership);
|
||||||
|
}
|
||||||
|
|
||||||
|
membership.Role = role;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task EnsureWorkspaceDataAsync(
|
private static async Task EnsureWorkspaceDataAsync(
|
||||||
Guid managerUserId,
|
Guid managerUserId,
|
||||||
Guid clientUserId,
|
Guid clientUserId,
|
||||||
@@ -231,33 +314,31 @@ public static class DevelopmentSeedExtensions
|
|||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Workspace? workspace = await dbContext.Workspaces
|
await UpsertWorkspaceAsync(
|
||||||
.SingleOrDefaultAsync(candidate => candidate.Id == WorkspaceId, cancellationToken);
|
dbContext,
|
||||||
if (workspace is null)
|
WorkspaceId,
|
||||||
{
|
OrganizationId,
|
||||||
workspace = new Workspace
|
managerUserId,
|
||||||
{
|
"Luma Coffee",
|
||||||
Id = WorkspaceId,
|
"America/Montreal",
|
||||||
Name = string.Empty,
|
"/images/seed/luma-coffee-logo.svg",
|
||||||
Slug = string.Empty,
|
cancellationToken);
|
||||||
TimeZone = string.Empty,
|
await UpsertWorkspaceAsync(
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
dbContext,
|
||||||
};
|
AtlasWorkspaceId,
|
||||||
dbContext.Workspaces.Add(workspace);
|
OrganizationId,
|
||||||
}
|
managerUserId,
|
||||||
|
"Atlas Bakery",
|
||||||
workspace.Name = "Northstar Studio";
|
"America/Montreal",
|
||||||
workspace.Slug = "northstar-studio";
|
"/images/seed/atlas-bakery-logo.svg",
|
||||||
workspace.OwnerUserId = managerUserId;
|
cancellationToken);
|
||||||
workspace.TimeZone = "America/Montreal";
|
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
|
||||||
|
|
||||||
await UpsertClientAsync(
|
await UpsertClientAsync(
|
||||||
dbContext,
|
dbContext,
|
||||||
ScopedClientId,
|
ScopedClientId,
|
||||||
"Luma Coffee",
|
"Luma Coffee",
|
||||||
"Active",
|
"Active",
|
||||||
"https://images.unsplash.com/photo-1511920170033-f8396924c348?auto=format&fit=crop&w=200&q=80",
|
"/images/seed/luma-coffee-logo.svg",
|
||||||
"Sofia Martin",
|
"Sofia Martin",
|
||||||
"client@socialize.local",
|
"client@socialize.local",
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
@@ -267,10 +348,10 @@ public static class DevelopmentSeedExtensions
|
|||||||
HiddenClientId,
|
HiddenClientId,
|
||||||
"Atlas Bakery",
|
"Atlas Bakery",
|
||||||
"Active",
|
"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 Cole",
|
||||||
"nina@atlasbakery.test",
|
"nina@atlasbakery.test",
|
||||||
WorkspaceId,
|
AtlasWorkspaceId,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
await UpsertCampaignAsync(
|
await UpsertCampaignAsync(
|
||||||
@@ -288,7 +369,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
await UpsertCampaignAsync(
|
await UpsertCampaignAsync(
|
||||||
dbContext,
|
dbContext,
|
||||||
HiddenCampaignId,
|
HiddenCampaignId,
|
||||||
WorkspaceId,
|
AtlasWorkspaceId,
|
||||||
HiddenClientId,
|
HiddenClientId,
|
||||||
"Summer Retention",
|
"Summer Retention",
|
||||||
"Planned",
|
"Planned",
|
||||||
@@ -298,6 +379,34 @@ public static class DevelopmentSeedExtensions
|
|||||||
"Sequence email and paid social updates together.",
|
"Sequence email and paid social updates together.",
|
||||||
cancellationToken);
|
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(
|
await UpsertContentItemAsync(
|
||||||
dbContext,
|
dbContext,
|
||||||
ScopedContentItemId,
|
ScopedContentItemId,
|
||||||
@@ -306,7 +415,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
ScopedCampaignId,
|
ScopedCampaignId,
|
||||||
"Spring launch hero video",
|
"Spring launch hero video",
|
||||||
"Fresh seasonal menu launch across Instagram and TikTok.",
|
"Fresh seasonal menu launch across Instagram and TikTok.",
|
||||||
"Instagram Reel, TikTok",
|
"Luma Coffee Instagram, Luma Coffee TikTok",
|
||||||
"In approval",
|
"In approval",
|
||||||
DateTimeOffset.UtcNow.AddDays(3),
|
DateTimeOffset.UtcNow.AddDays(3),
|
||||||
"v3",
|
"v3",
|
||||||
@@ -315,22 +424,22 @@ public static class DevelopmentSeedExtensions
|
|||||||
await UpsertContentItemAsync(
|
await UpsertContentItemAsync(
|
||||||
dbContext,
|
dbContext,
|
||||||
HiddenContentItemId,
|
HiddenContentItemId,
|
||||||
WorkspaceId,
|
AtlasWorkspaceId,
|
||||||
HiddenClientId,
|
HiddenClientId,
|
||||||
HiddenCampaignId,
|
HiddenCampaignId,
|
||||||
"Bakery loyalty carousel",
|
"Bakery loyalty carousel",
|
||||||
"Reward regular customers with a four-card retention carousel.",
|
"Reward regular customers with a four-card retention carousel.",
|
||||||
"Instagram Carousel",
|
"Atlas Bakery Instagram",
|
||||||
"Draft",
|
"Draft",
|
||||||
DateTimeOffset.UtcNow.AddDays(10),
|
DateTimeOffset.UtcNow.AddDays(10),
|
||||||
"v1",
|
"v1",
|
||||||
1,
|
1,
|
||||||
cancellationToken);
|
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-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.", "Instagram Reel, TikTok", "Addressed internal pacing feedback.", providerUserId, DateTimeOffset.UtcNow.AddDays(-3), 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.", "Instagram Reel, TikTok", "Client-facing draft after copy cleanup.", providerUserId, DateTimeOffset.UtcNow.AddDays(-1), 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.", "Instagram Carousel", "First draft.", managerUserId, DateTimeOffset.UtcNow.AddDays(-2), 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);
|
Asset? asset = await dbContext.Assets.SingleOrDefaultAsync(candidate => candidate.Id == ScopedAssetId, cancellationToken);
|
||||||
if (asset is null)
|
if (asset is null)
|
||||||
@@ -378,8 +487,6 @@ public static class DevelopmentSeedExtensions
|
|||||||
comment.AuthorDisplayName = "Sofia Martin";
|
comment.AuthorDisplayName = "Sofia Martin";
|
||||||
comment.AuthorEmail = "client@socialize.local";
|
comment.AuthorEmail = "client@socialize.local";
|
||||||
comment.Body = "Please tighten the opening three seconds and make the launch CTA more explicit.";
|
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);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
ApprovalRequest? approvalRequest = await dbContext.ApprovalRequests.SingleOrDefaultAsync(candidate => candidate.Id == ScopedApprovalRequestId, cancellationToken);
|
ApprovalRequest? approvalRequest = await dbContext.ApprovalRequests.SingleOrDefaultAsync(candidate => candidate.Id == ScopedApprovalRequestId, cancellationToken);
|
||||||
@@ -458,6 +565,38 @@ public static class DevelopmentSeedExtensions
|
|||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
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(
|
private static async Task UpsertClientAsync(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
Guid id,
|
Guid id,
|
||||||
@@ -527,6 +666,37 @@ public static class DevelopmentSeedExtensions
|
|||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
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(
|
private static async Task UpsertContentItemAsync(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
Guid id,
|
Guid id,
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Socialize.Api.Modules.Identity.Contracts;
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
using Socialize.Api.Modules.Organizations.Services;
|
||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public sealed class AccessScopeService
|
public sealed class AccessScopeService(
|
||||||
|
OrganizationAccessService organizationAccessService)
|
||||||
{
|
{
|
||||||
public bool IsManager(ClaimsPrincipal user)
|
public bool IsManager(ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
@@ -53,4 +55,123 @@ public sealed class AccessScopeService
|
|||||||
|| IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|
|| IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|
||||||
|| IsClient(user) && CanAccessClient(user, workspaceId, clientId);
|
|| IsClient(user) && CanAccessClient(user, workspaceId, clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<IReadOnlyCollection<Guid>> GetAccessibleWorkspaceIdsAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
return organizationAccessService.GetAccessibleWorkspaceIdsAsync(user, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanAccessWorkspaceAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
Guid workspaceId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
return CanAccessWorkspace(user, workspaceId)
|
||||||
|
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync(
|
||||||
|
user,
|
||||||
|
workspaceId,
|
||||||
|
OrganizationPermissions.AccessOwnedWorkspaces,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanManageWorkspaceAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
Guid workspaceId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
return IsManager(user)
|
||||||
|
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync(
|
||||||
|
user,
|
||||||
|
workspaceId,
|
||||||
|
OrganizationPermissions.ManageWorkspaces,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanCreateWorkspaceAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
Guid organizationId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
return IsManager(user)
|
||||||
|
|| await organizationAccessService.HasOrganizationPermissionAsync(
|
||||||
|
user,
|
||||||
|
organizationId,
|
||||||
|
OrganizationPermissions.CreateWorkspaces,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanAccessClientAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
Guid workspaceId,
|
||||||
|
Guid clientId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (IsManager(user) ||
|
||||||
|
await organizationAccessService.HasInheritedWorkspacePermissionAsync(
|
||||||
|
user,
|
||||||
|
workspaceId,
|
||||||
|
OrganizationPermissions.AccessOwnedWorkspaces,
|
||||||
|
ct))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user.GetWorkspaceScopeIds().Contains(workspaceId) && user.GetClientScopeIds().Contains(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanAccessCampaignAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
Guid workspaceId,
|
||||||
|
Guid clientId,
|
||||||
|
Guid campaignId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (IsManager(user) ||
|
||||||
|
await organizationAccessService.HasInheritedWorkspacePermissionAsync(
|
||||||
|
user,
|
||||||
|
workspaceId,
|
||||||
|
OrganizationPermissions.AccessOwnedWorkspaces,
|
||||||
|
ct))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await CanAccessClientAsync(user, workspaceId, clientId, ct) &&
|
||||||
|
user.GetCampaignScopeIds().Contains(campaignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanContributeToCampaignAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
Guid workspaceId,
|
||||||
|
Guid clientId,
|
||||||
|
Guid campaignId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
return IsManager(user)
|
||||||
|
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync(
|
||||||
|
user,
|
||||||
|
workspaceId,
|
||||||
|
OrganizationPermissions.ManageWorkspaces,
|
||||||
|
ct)
|
||||||
|
|| IsProvider(user) && await CanAccessCampaignAsync(user, workspaceId, clientId, campaignId, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanReviewContentAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
Guid workspaceId,
|
||||||
|
Guid clientId,
|
||||||
|
Guid campaignId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
return IsManager(user)
|
||||||
|
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync(
|
||||||
|
user,
|
||||||
|
workspaceId,
|
||||||
|
OrganizationPermissions.AccessOwnedWorkspaces,
|
||||||
|
ct)
|
||||||
|
|| IsProvider(user) && await CanAccessCampaignAsync(user, workspaceId, clientId, campaignId, ct)
|
||||||
|
|| IsClient(user) && await CanAccessClientAsync(user, workspaceId, clientId, ct);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,8 +12,8 @@ using Socialize.Api.Data;
|
|||||||
namespace Socialize.Api.Migrations
|
namespace Socialize.Api.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDbContext))]
|
[DbContext(typeof(AppDbContext))]
|
||||||
[Migration("20260501191447_RenameProjectsToCampaigns")]
|
[Migration("20260505204545_Initial")]
|
||||||
partial class RenameProjectsToCampaigns
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@@ -441,6 +441,326 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("AssetRevisions", (string)null);
|
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 =>
|
modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -494,6 +814,48 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("Campaigns", (string)null);
|
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 =>
|
modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -550,6 +912,29 @@ namespace Socialize.Api.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobContainerName")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobName")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobUrl")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentContentType")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentFileName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<long?>("AttachmentSizeBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
b.Property<string>("AuthorDisplayName")
|
b.Property<string>("AuthorDisplayName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
@@ -576,15 +961,9 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
b.Property<bool>("IsResolved")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<Guid?>("ParentCommentId")
|
b.Property<Guid?>("ParentCommentId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ResolvedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid>("WorkspaceId")
|
b.Property<Guid>("WorkspaceId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -665,6 +1044,62 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("ContentItems", (string)null);
|
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 =>
|
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -1206,6 +1641,70 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("NotificationEvents", (string)null);
|
b.ToTable("NotificationEvents", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.Organization", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.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)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid>("OwnerUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerUserId");
|
||||||
|
|
||||||
|
b.ToTable("Organizations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Role")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId", "UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("OrganizationMemberships", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -1238,6 +1737,9 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(256)");
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<Guid>("OwnerUserId")
|
b.Property<Guid>("OwnerUserId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -1251,11 +1753,6 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasDefaultValue(false);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<string>("TimeZone")
|
b.Property<string>("TimeZone")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
@@ -1263,10 +1760,9 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("OwnerUserId");
|
b.HasIndex("OrganizationId");
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
b.HasIndex("OwnerUserId");
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Workspaces", (string)null);
|
b.ToTable("Workspaces", (string)null);
|
||||||
});
|
});
|
||||||
@@ -1363,6 +1859,15 @@ namespace Socialize.Api.Migrations
|
|||||||
.IsRequired();
|
.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 =>
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||||
@@ -1407,6 +1912,24 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Navigation("FeedbackReport");
|
b.Navigation("FeedbackReport");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrganizationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrganizationId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("ActivityEntries");
|
b.Navigation("ActivityEntries");
|
||||||
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);
|
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 =>
|
modelBuilder.Entity("Socialize.Api.Modules.Campaigns.Data.Campaign", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -491,6 +811,48 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("Campaigns", (string)null);
|
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 =>
|
modelBuilder.Entity("Socialize.Api.Modules.Clients.Data.Client", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -547,6 +909,29 @@ namespace Socialize.Api.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobContainerName")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobName")
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentBlobUrl")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentContentType")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("AttachmentFileName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<long?>("AttachmentSizeBytes")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
b.Property<string>("AuthorDisplayName")
|
b.Property<string>("AuthorDisplayName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
@@ -573,15 +958,9 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("timestamp with time zone")
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
b.Property<bool>("IsResolved")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.Property<Guid?>("ParentCommentId")
|
b.Property<Guid?>("ParentCommentId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<DateTimeOffset?>("ResolvedAt")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<Guid>("WorkspaceId")
|
b.Property<Guid>("WorkspaceId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -662,6 +1041,62 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("ContentItems", (string)null);
|
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 =>
|
modelBuilder.Entity("Socialize.Api.Modules.ContentItems.Data.ContentItemRevision", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -1203,6 +1638,70 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("NotificationEvents", (string)null);
|
b.ToTable("NotificationEvents", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.Organization", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.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)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid>("OwnerUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerUserId");
|
||||||
|
|
||||||
|
b.ToTable("Organizations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Role")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId", "UserId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("OrganizationMemberships", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -1235,6 +1734,9 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
.HasColumnType("character varying(256)");
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<Guid>("OwnerUserId")
|
b.Property<Guid>("OwnerUserId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -1248,11 +1750,6 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasDefaultValue(false);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
|
||||||
.IsRequired()
|
|
||||||
.HasMaxLength(128)
|
|
||||||
.HasColumnType("character varying(128)");
|
|
||||||
|
|
||||||
b.Property<string>("TimeZone")
|
b.Property<string>("TimeZone")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
@@ -1260,10 +1757,9 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("OwnerUserId");
|
b.HasIndex("OrganizationId");
|
||||||
|
|
||||||
b.HasIndex("Slug")
|
b.HasIndex("OwnerUserId");
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Workspaces", (string)null);
|
b.ToTable("Workspaces", (string)null);
|
||||||
});
|
});
|
||||||
@@ -1360,6 +1856,15 @@ namespace Socialize.Api.Migrations
|
|||||||
.IsRequired();
|
.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 =>
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackActivityEntry", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
b.HasOne("Socialize.Api.Modules.Feedback.Data.FeedbackReport", "FeedbackReport")
|
||||||
@@ -1404,6 +1909,24 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Navigation("FeedbackReport");
|
b.Navigation("FeedbackReport");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.OrganizationMembership", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrganizationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Workspaces.Data.Workspace", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Socialize.Api.Modules.Organizations.Data.Organization", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrganizationId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Feedback.Data.FeedbackReport", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("ActivityEntries");
|
b.Navigation("ActivityEntries");
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
using FastEndpoints;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using Socialize.Api.Data;
|
|
||||||
using Socialize.Api.Infrastructure.Security;
|
|
||||||
using Socialize.Api.Modules.Approvals.Data;
|
|
||||||
using Socialize.Api.Modules.Approvals.Services;
|
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
|
||||||
|
|
||||||
public record CreateApprovalRequestRequest(
|
|
||||||
Guid WorkspaceId,
|
|
||||||
Guid ContentItemId,
|
|
||||||
string Stage,
|
|
||||||
string ReviewerName,
|
|
||||||
string ReviewerEmail,
|
|
||||||
DateTimeOffset? DueAt);
|
|
||||||
|
|
||||||
public class CreateApprovalRequestRequestValidator
|
|
||||||
: Validator<CreateApprovalRequestRequest>
|
|
||||||
{
|
|
||||||
public CreateApprovalRequestRequestValidator()
|
|
||||||
{
|
|
||||||
RuleFor(x => x.WorkspaceId).NotEmpty();
|
|
||||||
RuleFor(x => x.ContentItemId).NotEmpty();
|
|
||||||
RuleFor(x => x.Stage).NotEmpty().MaximumLength(64);
|
|
||||||
RuleFor(x => x.ReviewerName).NotEmpty().MaximumLength(256);
|
|
||||||
RuleFor(x => x.ReviewerEmail).NotEmpty().MaximumLength(256).EmailAddress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class CreateApprovalRequestHandler(
|
|
||||||
AppDbContext dbContext,
|
|
||||||
AccessScopeService accessScopeService,
|
|
||||||
INotificationEventWriter notificationEventWriter)
|
|
||||||
: Endpoint<CreateApprovalRequestRequest, ApprovalRequestDto>
|
|
||||||
{
|
|
||||||
public override void Configure()
|
|
||||||
{
|
|
||||||
Post("/api/approvals");
|
|
||||||
Options(o => o.WithTags("Approvals"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task HandleAsync(CreateApprovalRequestRequest request, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var contentItem = await dbContext
|
|
||||||
.ContentItems
|
|
||||||
.SingleOrDefaultAsync(
|
|
||||||
candidate => candidate.Id == request.ContentItemId && candidate.WorkspaceId == request.WorkspaceId,
|
|
||||||
ct);
|
|
||||||
|
|
||||||
if (contentItem is null)
|
|
||||||
{
|
|
||||||
AddError(request => request.ContentItemId, "The selected content item does not exist in the active workspace.");
|
|
||||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!accessScopeService.CanManageWorkspace(User, contentItem.WorkspaceId))
|
|
||||||
{
|
|
||||||
await SendForbiddenAsync(ct);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Workspace? workspace = await dbContext.Workspaces.SingleOrDefaultAsync(candidate => candidate.Id == contentItem.WorkspaceId, ct);
|
|
||||||
if (workspace is null)
|
|
||||||
{
|
|
||||||
await SendNotFoundAsync(ct);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ApprovalWorkflowRules.CanCreateSingleStepApprovalRequest(workspace.ApprovalMode))
|
|
||||||
{
|
|
||||||
AddError(request => request.WorkspaceId, workspace.ApprovalMode == ApprovalModes.None
|
|
||||||
? "Approval workflow is disabled for this workspace."
|
|
||||||
: "Move content to In approval to start the configured multi-level approval workflow.");
|
|
||||||
await SendErrorsAsync(StatusCodes.Status400BadRequest, ct);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var approval = new ApprovalRequest()
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
WorkspaceId = request.WorkspaceId,
|
|
||||||
ContentItemId = request.ContentItemId,
|
|
||||||
Stage = request.Stage.Trim(),
|
|
||||||
ReviewerName = request.ReviewerName.Trim(),
|
|
||||||
ReviewerEmail = request.ReviewerEmail.Trim(),
|
|
||||||
RequestedByUserId = User.GetUserId(),
|
|
||||||
DueAt = request.DueAt,
|
|
||||||
State = "Pending",
|
|
||||||
AccessToken = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLowerInvariant(),
|
|
||||||
SentAt = DateTimeOffset.UtcNow,
|
|
||||||
};
|
|
||||||
|
|
||||||
dbContext.ApprovalRequests.Add(approval);
|
|
||||||
|
|
||||||
contentItem.Status = "In approval";
|
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync(ct);
|
|
||||||
|
|
||||||
await notificationEventWriter.WriteAsync(
|
|
||||||
new NotificationEventWriteModel(
|
|
||||||
approval.WorkspaceId,
|
|
||||||
approval.ContentItemId,
|
|
||||||
"approval.requested",
|
|
||||||
"ApprovalRequest",
|
|
||||||
approval.Id,
|
|
||||||
$"Approval requested from {approval.ReviewerName} for {contentItem.Title}.",
|
|
||||||
null,
|
|
||||||
approval.ReviewerEmail,
|
|
||||||
$$"""{"stage":"{{approval.Stage}}","accessToken":"{{approval.AccessToken}}"}"""),
|
|
||||||
ct);
|
|
||||||
|
|
||||||
ApprovalRequestDto dto = new(
|
|
||||||
approval.Id,
|
|
||||||
approval.WorkspaceId,
|
|
||||||
approval.ContentItemId,
|
|
||||||
approval.WorkflowInstanceId,
|
|
||||||
approval.WorkflowStepSortOrder,
|
|
||||||
approval.WorkflowStepTargetType,
|
|
||||||
approval.WorkflowStepTargetValue,
|
|
||||||
approval.WorkflowStepRequiredApproverCount,
|
|
||||||
approval.Stage,
|
|
||||||
approval.ReviewerName,
|
|
||||||
approval.ReviewerEmail,
|
|
||||||
approval.RequestedByUserId,
|
|
||||||
approval.DueAt,
|
|
||||||
approval.State,
|
|
||||||
approval.AccessToken,
|
|
||||||
approval.SentAt,
|
|
||||||
approval.CompletedAt,
|
|
||||||
[]);
|
|
||||||
|
|
||||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -61,7 +61,7 @@ public class GetApprovalsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||||
using Socialize.Api.Modules.Approvals.Data;
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
using Socialize.Api.Modules.Approvals.Services;
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||||
|
|
||||||
public record SubmitApprovalDecisionRequest(
|
public record SubmitApprovalDecisionRequest(
|
||||||
string Decision,
|
string Decision,
|
||||||
string? Comment,
|
|
||||||
string? ReviewerName,
|
string? ReviewerName,
|
||||||
string? ReviewerEmail);
|
string? ReviewerEmail);
|
||||||
|
|
||||||
@@ -25,7 +26,6 @@ public class SubmitApprovalDecisionRequestValidator
|
|||||||
.NotEmpty()
|
.NotEmpty()
|
||||||
.Equal("Approved")
|
.Equal("Approved")
|
||||||
.WithMessage("Only approved decisions are supported.");
|
.WithMessage("Only approved decisions are supported.");
|
||||||
RuleFor(x => x.Comment).MaximumLength(2048);
|
|
||||||
RuleFor(x => x.ReviewerName).MaximumLength(256);
|
RuleFor(x => x.ReviewerName).MaximumLength(256);
|
||||||
RuleFor(x => x.ReviewerEmail).MaximumLength(256).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ReviewerEmail));
|
RuleFor(x => x.ReviewerEmail).MaximumLength(256).EmailAddress().When(x => !string.IsNullOrWhiteSpace(x.ReviewerEmail));
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,7 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||||
|
IContentItemActivityWriter activityWriter,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<SubmitApprovalDecisionRequest, ApprovalRequestDto>
|
: Endpoint<SubmitApprovalDecisionRequest, ApprovalRequestDto>
|
||||||
{
|
{
|
||||||
@@ -64,7 +65,7 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (User?.Identity?.IsAuthenticated == true &&
|
if (User?.Identity?.IsAuthenticated == true &&
|
||||||
!accessScopeService.CanReviewContent(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId))
|
!await accessScopeService.CanReviewContentAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -90,7 +91,7 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
ApprovalRequestId = approval.Id,
|
ApprovalRequestId = approval.Id,
|
||||||
Decision = normalizedDecision,
|
Decision = normalizedDecision,
|
||||||
Comment = string.IsNullOrWhiteSpace(request.Comment) ? null : request.Comment.Trim(),
|
Comment = null,
|
||||||
DecidedByUserId = User?.Identity?.IsAuthenticated == true ? User.GetUserId() : null,
|
DecidedByUserId = User?.Identity?.IsAuthenticated == true ? User.GetUserId() : null,
|
||||||
DecidedByName = decidedByName,
|
DecidedByName = decidedByName,
|
||||||
DecidedByEmail = decidedByEmail,
|
DecidedByEmail = decidedByEmail,
|
||||||
@@ -122,6 +123,24 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
dbContext.ApprovalDecisions.Add(decision);
|
dbContext.ApprovalDecisions.Add(decision);
|
||||||
await dbContext.SaveChangesAsync(ct);
|
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(
|
await notificationEventWriter.WriteAsync(
|
||||||
new NotificationEventWriteModel(
|
new NotificationEventWriteModel(
|
||||||
approval.WorkspaceId,
|
approval.WorkspaceId,
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ public static class ApprovalModes
|
|||||||
|
|
||||||
public static class ApprovalWorkflowRules
|
public static class ApprovalWorkflowRules
|
||||||
{
|
{
|
||||||
public static bool CanCreateSingleStepApprovalRequest(string approvalMode)
|
|
||||||
{
|
|
||||||
return approvalMode is ApprovalModes.Optional or ApprovalModes.Required;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool BlocksManualApprovedOrScheduledStatus(string approvalMode)
|
public static bool BlocksManualApprovedOrScheduledStatus(string approvalMode)
|
||||||
{
|
{
|
||||||
return approvalMode is ApprovalModes.Required or ApprovalModes.MultiLevel;
|
return approvalMode is ApprovalModes.Required or ApprovalModes.MultiLevel;
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
using Socialize.Api.Modules.Assets.Data;
|
using Socialize.Api.Modules.Assets.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ public class CreateAssetRevisionRequestValidator
|
|||||||
public class CreateAssetRevisionHandler(
|
public class CreateAssetRevisionHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
|
IContentItemActivityWriter activityWriter,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<CreateAssetRevisionRequest, AssetRevisionDto>
|
: Endpoint<CreateAssetRevisionRequest, AssetRevisionDto>
|
||||||
{
|
{
|
||||||
@@ -51,7 +54,7 @@ public class CreateAssetRevisionHandler(
|
|||||||
.SingleOrDefaultAsync(candidate => candidate.Id == asset.ContentItemId, ct);
|
.SingleOrDefaultAsync(candidate => candidate.Id == asset.ContentItemId, ct);
|
||||||
|
|
||||||
if (contentItem is not null &&
|
if (contentItem is not null &&
|
||||||
!accessScopeService.CanContributeToCampaign(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId))
|
!await accessScopeService.CanContributeToCampaignAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -78,6 +81,25 @@ public class CreateAssetRevisionHandler(
|
|||||||
|
|
||||||
if (contentItem is not null)
|
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(
|
await notificationEventWriter.WriteAsync(
|
||||||
new NotificationEventWriteModel(
|
new NotificationEventWriteModel(
|
||||||
asset.WorkspaceId,
|
asset.WorkspaceId,
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
using Socialize.Api.Modules.Assets.Data;
|
using Socialize.Api.Modules.Assets.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
@@ -35,6 +37,7 @@ public class CreateGoogleDriveAssetRequestValidator
|
|||||||
public class CreateGoogleDriveAssetHandler(
|
public class CreateGoogleDriveAssetHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
|
IContentItemActivityWriter activityWriter,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<CreateGoogleDriveAssetRequest, AssetDto>
|
: Endpoint<CreateGoogleDriveAssetRequest, AssetDto>
|
||||||
{
|
{
|
||||||
@@ -58,7 +61,7 @@ public class CreateGoogleDriveAssetHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanContributeToCampaign(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId))
|
if (!await accessScopeService.CanContributeToCampaignAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -93,6 +96,25 @@ public class CreateGoogleDriveAssetHandler(
|
|||||||
dbContext.AssetRevisions.Add(revision);
|
dbContext.AssetRevisions.Add(revision);
|
||||||
await dbContext.SaveChangesAsync(ct);
|
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(
|
await notificationEventWriter.WriteAsync(
|
||||||
new NotificationEventWriteModel(
|
new NotificationEventWriteModel(
|
||||||
asset.WorkspaceId,
|
asset.WorkspaceId,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class GetAssetsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,7 +45,7 @@ public class CreateCampaignHandler(
|
|||||||
|
|
||||||
public override async Task HandleAsync(CreateCampaignRequest request, CancellationToken ct)
|
public override async Task HandleAsync(CreateCampaignRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!accessScopeService.CanManageWorkspace(User, request.WorkspaceId))
|
if (!await accessScopeService.CanManageWorkspaceAsync(User, request.WorkspaceId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -34,16 +34,9 @@ public class GetCampaignsHandler(
|
|||||||
{
|
{
|
||||||
IQueryable<Campaign> query = dbContext.Campaigns.AsQueryable();
|
IQueryable<Campaign> query = dbContext.Campaigns.AsQueryable();
|
||||||
|
|
||||||
if (accessScopeService.IsManager(User))
|
if (!accessScopeService.IsManager(User))
|
||||||
{
|
{
|
||||||
if (request.WorkspaceId.HasValue)
|
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||||
{
|
|
||||||
query = query.Where(campaign => campaign.WorkspaceId == request.WorkspaceId.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
|
|
||||||
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
||||||
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
|
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
|
||||||
|
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ public class ChangeClientPortraitHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanManageWorkspace(User, client.WorkspaceId))
|
if (!await accessScopeService.CanManageWorkspaceAsync(User, client.WorkspaceId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class CreateClientHandler(
|
|||||||
|
|
||||||
public override async Task HandleAsync(CreateClientRequest request, CancellationToken ct)
|
public override async Task HandleAsync(CreateClientRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!accessScopeService.CanManageWorkspace(User, request.WorkspaceId))
|
if (!await accessScopeService.CanManageWorkspaceAsync(User, request.WorkspaceId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -33,16 +33,9 @@ public class GetClientsHandler(
|
|||||||
{
|
{
|
||||||
IQueryable<Client> query = dbContext.Clients.AsQueryable();
|
IQueryable<Client> query = dbContext.Clients.AsQueryable();
|
||||||
|
|
||||||
if (accessScopeService.IsManager(User))
|
if (!accessScopeService.IsManager(User))
|
||||||
{
|
{
|
||||||
if (request.WorkspaceId.HasValue)
|
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||||
{
|
|
||||||
query = query.Where(client => client.WorkspaceId == request.WorkspaceId.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
|
|
||||||
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
||||||
|
|
||||||
query = query.Where(client => workspaceScopeIds.Contains(client.WorkspaceId));
|
query = query.Where(client => workspaceScopeIds.Contains(client.WorkspaceId));
|
||||||
@@ -52,10 +45,11 @@ public class GetClientsHandler(
|
|||||||
query = query.Where(client => clientScopeIds.Contains(client.Id));
|
query = query.Where(client => clientScopeIds.Contains(client.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.WorkspaceId.HasValue)
|
}
|
||||||
{
|
|
||||||
query = query.Where(client => client.WorkspaceId == request.WorkspaceId.Value);
|
if (request.WorkspaceId.HasValue)
|
||||||
}
|
{
|
||||||
|
query = query.Where(client => client.WorkspaceId == request.WorkspaceId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ClientDto> clients = await query
|
List<ClientDto> clients = await query
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ public class UpdateClientHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanManageWorkspace(User, client.WorkspaceId))
|
if (!await accessScopeService.CanManageWorkspaceAsync(User, client.WorkspaceId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ public class Comment
|
|||||||
public required string AuthorDisplayName { get; set; }
|
public required string AuthorDisplayName { get; set; }
|
||||||
public required string AuthorEmail { get; set; }
|
public required string AuthorEmail { get; set; }
|
||||||
public required string Body { 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 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.AuthorDisplayName).HasMaxLength(256).IsRequired();
|
||||||
comment.Property(x => x.AuthorEmail).HasMaxLength(256).IsRequired();
|
comment.Property(x => x.AuthorEmail).HasMaxLength(256).IsRequired();
|
||||||
comment.Property(x => x.Body).HasMaxLength(4000).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)
|
comment.Property(x => x.CreatedAt)
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
using FastEndpoints;
|
using FastEndpoints;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Comments.Data;
|
using Socialize.Api.Modules.Comments.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Comments.Handlers;
|
namespace Socialize.Api.Modules.Comments.Handlers;
|
||||||
|
|
||||||
@@ -12,7 +15,8 @@ public record CreateCommentRequest(
|
|||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ContentItemId,
|
Guid ContentItemId,
|
||||||
Guid? ParentCommentId,
|
Guid? ParentCommentId,
|
||||||
string Body);
|
string Body,
|
||||||
|
IFormFile? Attachment);
|
||||||
|
|
||||||
public class CreateCommentRequestValidator
|
public class CreateCommentRequestValidator
|
||||||
: Validator<CreateCommentRequest>
|
: Validator<CreateCommentRequest>
|
||||||
@@ -21,13 +25,15 @@ public class CreateCommentRequestValidator
|
|||||||
{
|
{
|
||||||
RuleFor(x => x.WorkspaceId).NotEmpty();
|
RuleFor(x => x.WorkspaceId).NotEmpty();
|
||||||
RuleFor(x => x.ContentItemId).NotEmpty();
|
RuleFor(x => x.ContentItemId).NotEmpty();
|
||||||
RuleFor(x => x.Body).NotEmpty().MaximumLength(4000);
|
RuleFor(x => x.Body).MaximumLength(4000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateCommentHandler(
|
public class CreateCommentHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
|
IBlobStorage blobStorage,
|
||||||
|
IContentItemActivityWriter activityWriter,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<CreateCommentRequest, CommentDto>
|
: Endpoint<CreateCommentRequest, CommentDto>
|
||||||
{
|
{
|
||||||
@@ -35,10 +41,19 @@ public class CreateCommentHandler(
|
|||||||
{
|
{
|
||||||
Post("/api/comments");
|
Post("/api/comments");
|
||||||
Options(o => o.WithTags("Comments"));
|
Options(o => o.WithTags("Comments"));
|
||||||
|
AllowFileUploads();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task HandleAsync(CreateCommentRequest request, CancellationToken ct)
|
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
|
ContentItem? contentItem = await dbContext.ContentItems
|
||||||
.SingleOrDefaultAsync(
|
.SingleOrDefaultAsync(
|
||||||
candidate => candidate.Id == request.ContentItemId && candidate.WorkspaceId == request.WorkspaceId,
|
candidate => candidate.Id == request.ContentItemId && candidate.WorkspaceId == request.WorkspaceId,
|
||||||
@@ -51,7 +66,7 @@ public class CreateCommentHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId))
|
if (!await accessScopeService.CanReviewContentAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -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()
|
Comment comment = new()
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = commentId,
|
||||||
WorkspaceId = request.WorkspaceId,
|
WorkspaceId = request.WorkspaceId,
|
||||||
ContentItemId = request.ContentItemId,
|
ContentItemId = request.ContentItemId,
|
||||||
ParentCommentId = request.ParentCommentId,
|
ParentCommentId = request.ParentCommentId,
|
||||||
AuthorUserId = User.GetUserId(),
|
AuthorUserId = User.GetUserId(),
|
||||||
AuthorDisplayName = User.GetAlias() ?? User.GetName(),
|
AuthorDisplayName = User.GetAlias() ?? User.GetName(),
|
||||||
AuthorEmail = User.GetEmail(),
|
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,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,6 +162,23 @@ public class CreateCommentHandler(
|
|||||||
.Select(candidate => candidate.PortraitUrl)
|
.Select(candidate => candidate.PortraitUrl)
|
||||||
.SingleOrDefaultAsync(ct);
|
.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(
|
await notificationEventWriter.WriteAsync(
|
||||||
new NotificationEventWriteModel(
|
new NotificationEventWriteModel(
|
||||||
comment.WorkspaceId,
|
comment.WorkspaceId,
|
||||||
@@ -116,10 +202,34 @@ public class CreateCommentHandler(
|
|||||||
comment.AuthorEmail,
|
comment.AuthorEmail,
|
||||||
authorPortraitUrl,
|
authorPortraitUrl,
|
||||||
comment.Body,
|
comment.Body,
|
||||||
comment.IsResolved,
|
comment.AttachmentFileName,
|
||||||
comment.CreatedAt,
|
comment.AttachmentContentType,
|
||||||
comment.ResolvedAt);
|
comment.AttachmentSizeBytes,
|
||||||
|
comment.AttachmentBlobUrl,
|
||||||
|
comment.CreatedAt);
|
||||||
|
|
||||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
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 AuthorEmail,
|
||||||
string? AuthorPortraitUrl,
|
string? AuthorPortraitUrl,
|
||||||
string Body,
|
string Body,
|
||||||
bool IsResolved,
|
string? AttachmentFileName,
|
||||||
DateTimeOffset CreatedAt,
|
string? AttachmentContentType,
|
||||||
DateTimeOffset? ResolvedAt);
|
long? AttachmentSizeBytes,
|
||||||
|
string? AttachmentBlobUrl,
|
||||||
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
public class GetCommentsHandler(
|
public class GetCommentsHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
@@ -44,7 +46,7 @@ public class GetCommentsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -75,9 +77,11 @@ public class GetCommentsHandler(
|
|||||||
comment.AuthorEmail,
|
comment.AuthorEmail,
|
||||||
authorPortraits.GetValueOrDefault(comment.AuthorUserId),
|
authorPortraits.GetValueOrDefault(comment.AuthorUserId),
|
||||||
comment.Body,
|
comment.Body,
|
||||||
comment.IsResolved,
|
comment.AttachmentFileName,
|
||||||
comment.CreatedAt,
|
comment.AttachmentContentType,
|
||||||
comment.ResolvedAt))
|
comment.AttachmentSizeBytes,
|
||||||
|
comment.AttachmentBlobUrl,
|
||||||
|
comment.CreatedAt))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
await SendOkAsync(dtos, ct);
|
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 = accessScopeService.CanManageWorkspace(User, comment.WorkspaceId)
|
|
||||||
|| accessScopeService.CanContributeToCampaign(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId);
|
|
||||||
|
|
||||||
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();
|
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;
|
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;
|
namespace Socialize.Api.Modules.ContentItems;
|
||||||
|
|
||||||
@@ -7,6 +8,8 @@ public static class DependencyInjection
|
|||||||
public static WebApplicationBuilder AddContentItemsModule(
|
public static WebApplicationBuilder AddContentItemsModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
|
builder.Services.AddScoped<IContentItemActivityWriter, ContentItemActivityWriter>();
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ using FastEndpoints;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
@@ -36,6 +38,7 @@ public class CreateContentItemRequestValidator
|
|||||||
public class CreateContentItemHandler(
|
public class CreateContentItemHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
|
IContentItemActivityWriter activityWriter,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<CreateContentItemRequest, ContentItemDto>
|
: Endpoint<CreateContentItemRequest, ContentItemDto>
|
||||||
{
|
{
|
||||||
@@ -47,7 +50,7 @@ public class CreateContentItemHandler(
|
|||||||
|
|
||||||
public override async Task HandleAsync(CreateContentItemRequest request, CancellationToken ct)
|
public override async Task HandleAsync(CreateContentItemRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!accessScopeService.CanContributeToCampaign(User, request.WorkspaceId, request.ClientId, request.CampaignId))
|
if (!await accessScopeService.CanContributeToCampaignAsync(User, request.WorkspaceId, request.ClientId, request.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -121,6 +124,26 @@ public class CreateContentItemHandler(
|
|||||||
});
|
});
|
||||||
await dbContext.SaveChangesAsync(ct);
|
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(
|
await notificationEventWriter.WriteAsync(
|
||||||
new NotificationEventWriteModel(
|
new NotificationEventWriteModel(
|
||||||
item.WorkspaceId,
|
item.WorkspaceId,
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ using FastEndpoints;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
@@ -12,7 +14,8 @@ public record CreateContentItemRevisionRequest(
|
|||||||
string PublicationMessage,
|
string PublicationMessage,
|
||||||
string PublicationTargets,
|
string PublicationTargets,
|
||||||
string? Hashtags,
|
string? Hashtags,
|
||||||
string? ChangeSummary);
|
string? ChangeSummary,
|
||||||
|
DateTimeOffset? DueDate);
|
||||||
|
|
||||||
public class CreateContentItemRevisionRequestValidator
|
public class CreateContentItemRevisionRequestValidator
|
||||||
: Validator<CreateContentItemRevisionRequest>
|
: Validator<CreateContentItemRevisionRequest>
|
||||||
@@ -30,6 +33,7 @@ public class CreateContentItemRevisionRequestValidator
|
|||||||
public class CreateContentItemRevisionHandler(
|
public class CreateContentItemRevisionHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
|
IContentItemActivityWriter activityWriter,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<CreateContentItemRevisionRequest, ContentItemRevisionDto>
|
: Endpoint<CreateContentItemRevisionRequest, ContentItemRevisionDto>
|
||||||
{
|
{
|
||||||
@@ -50,7 +54,7 @@ public class CreateContentItemRevisionHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanContributeToCampaign(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
if (!await accessScopeService.CanContributeToCampaignAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -58,11 +62,21 @@ public class CreateContentItemRevisionHandler(
|
|||||||
|
|
||||||
int revisionNumber = item.CurrentRevisionNumber + 1;
|
int revisionNumber = item.CurrentRevisionNumber + 1;
|
||||||
string revisionLabel = $"v{revisionNumber}";
|
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.Title = newTitle;
|
||||||
item.PublicationMessage = request.PublicationMessage.Trim();
|
item.PublicationMessage = newPublicationMessage;
|
||||||
item.PublicationTargets = request.PublicationTargets.Trim();
|
item.PublicationTargets = newPublicationTargets;
|
||||||
item.Hashtags = string.IsNullOrWhiteSpace(request.Hashtags) ? null : request.Hashtags.Trim();
|
item.Hashtags = newHashtags;
|
||||||
|
item.DueDate = request.DueDate;
|
||||||
item.CurrentRevisionNumber = revisionNumber;
|
item.CurrentRevisionNumber = revisionNumber;
|
||||||
item.CurrentRevisionLabel = revisionLabel;
|
item.CurrentRevisionLabel = revisionLabel;
|
||||||
|
|
||||||
@@ -84,6 +98,32 @@ public class CreateContentItemRevisionHandler(
|
|||||||
dbContext.ContentItemRevisions.Add(revision);
|
dbContext.ContentItemRevisions.Add(revision);
|
||||||
await dbContext.SaveChangesAsync(ct);
|
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(
|
await notificationEventWriter.WriteAsync(
|
||||||
new NotificationEventWriteModel(
|
new NotificationEventWriteModel(
|
||||||
item.WorkspaceId,
|
item.WorkspaceId,
|
||||||
@@ -112,4 +152,19 @@ public class CreateContentItemRevisionHandler(
|
|||||||
|
|
||||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public class GetContentItemHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ public class GetContentItemRevisionsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public class GetContentItemsHandler(
|
|||||||
|
|
||||||
if (!accessScopeService.IsManager(User))
|
if (!accessScopeService.IsManager(User))
|
||||||
{
|
{
|
||||||
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
|
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||||
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
IReadOnlyCollection<Guid> clientScopeIds = User.GetClientScopeIds();
|
||||||
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
|
IReadOnlyCollection<Guid> campaignScopeIds = User.GetCampaignScopeIds();
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
using Socialize.Api.Modules.Approvals.Services;
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Contracts;
|
||||||
using Socialize.Api.Modules.ContentItems.Data;
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
using Socialize.Api.Modules.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
namespace Socialize.Api.Modules.ContentItems.Handlers;
|
||||||
|
|
||||||
@@ -24,6 +26,7 @@ public class UpdateContentItemStatusHandler(
|
|||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||||
|
IContentItemActivityWriter activityWriter,
|
||||||
INotificationEventWriter notificationEventWriter)
|
INotificationEventWriter notificationEventWriter)
|
||||||
: Endpoint<UpdateContentItemStatusRequest, ContentItemDetailDto>
|
: Endpoint<UpdateContentItemStatusRequest, ContentItemDetailDto>
|
||||||
{
|
{
|
||||||
@@ -54,7 +57,7 @@ public class UpdateContentItemStatusHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanManageWorkspace(User, item.WorkspaceId))
|
if (!await accessScopeService.CanManageWorkspaceAsync(User, item.WorkspaceId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -122,12 +125,33 @@ public class UpdateContentItemStatusHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string previousStatus = item.Status;
|
||||||
if (item.Status != "In approval" || normalizedStatus != "In approval")
|
if (item.Status != "In approval" || normalizedStatus != "In approval")
|
||||||
{
|
{
|
||||||
item.Status = normalizedStatus;
|
item.Status = normalizedStatus;
|
||||||
}
|
}
|
||||||
await dbContext.SaveChangesAsync(ct);
|
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(
|
await notificationEventWriter.WriteAsync(
|
||||||
new NotificationEventWriteModel(
|
new NotificationEventWriteModel(
|
||||||
item.WorkspaceId,
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ public class GetNotificationsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.CampaignId))
|
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -58,7 +58,7 @@ public class GetNotificationsHandler(
|
|||||||
|
|
||||||
if (!accessScopeService.IsManager(User))
|
if (!accessScopeService.IsManager(User))
|
||||||
{
|
{
|
||||||
IReadOnlyCollection<Guid> workspaceScopeIds = User.GetWorkspaceScopeIds();
|
IReadOnlyCollection<Guid> workspaceScopeIds = await accessScopeService.GetAccessibleWorkspaceIdsAsync(User, ct);
|
||||||
query = query.Where(notificationEvent =>
|
query = query.Where(notificationEvent =>
|
||||||
workspaceScopeIds.Contains(notificationEvent.WorkspaceId) ||
|
workspaceScopeIds.Contains(notificationEvent.WorkspaceId) ||
|
||||||
notificationEvent.RecipientUserId == currentUserId);
|
notificationEvent.RecipientUserId == currentUserId);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class MarkNotificationAsReadHandler(
|
|||||||
|
|
||||||
Guid currentUserId = User.GetUserId();
|
Guid currentUserId = User.GetUserId();
|
||||||
bool canReadRecipientNotification = notificationEvent.RecipientUserId == currentUserId;
|
bool canReadRecipientNotification = notificationEvent.RecipientUserId == currentUserId;
|
||||||
if (!canReadRecipientNotification && !accessScopeService.CanAccessWorkspace(User, notificationEvent.WorkspaceId))
|
if (!canReadRecipientNotification && !await accessScopeService.CanAccessWorkspaceAsync(User, notificationEvent.WorkspaceId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Socialize.Api.Modules.Organizations.Data;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Socialize.Api.Modules.Organizations.Data;
|
||||||
|
|
||||||
|
public class OrganizationMembership
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public required string Role { get; set; }
|
||||||
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Organizations.Data;
|
||||||
|
|
||||||
|
public static class OrganizationModelConfiguration
|
||||||
|
{
|
||||||
|
public static ModelBuilder ConfigureOrganizationsModule(this ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
modelBuilder.Entity<Organization>(organization =>
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
organization.HasIndex(x => x.OwnerUserId);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<OrganizationMembership>(membership =>
|
||||||
|
{
|
||||||
|
membership.ToTable("OrganizationMemberships");
|
||||||
|
membership.HasKey(x => x.Id);
|
||||||
|
membership.Property(x => x.Role).HasMaxLength(64).IsRequired();
|
||||||
|
membership.Property(x => x.CreatedAt)
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
membership.HasIndex(x => x.OrganizationId);
|
||||||
|
membership.HasIndex(x => x.UserId);
|
||||||
|
membership.HasIndex(x => new { x.OrganizationId, x.UserId }).IsUnique();
|
||||||
|
membership.HasOne<Organization>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.OrganizationId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
return modelBuilder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Socialize.Api.Modules.Organizations.Services;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Organizations;
|
||||||
|
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
public static WebApplicationBuilder AddOrganizationsModule(
|
||||||
|
this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
builder.Services.AddScoped<OrganizationAccessService>();
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user