Compare commits
92 Commits
work-in-pr
...
feat/prepr
| Author | SHA1 | Date | |
|---|---|---|---|
| 986c7efea6 | |||
| 8bcff96821 | |||
| 1ca6ab7117 | |||
| e81c9f42c9 | |||
| c527011646 | |||
| 0b7edb1b7f | |||
| dcfdce1ec6 | |||
| 2eb54b9228 | |||
| 9c011f1a1e | |||
| b6eb348605 | |||
| 7a8a0a44bf | |||
| 6d92119c9c | |||
| db16e79d9f | |||
| 4aaa1a7f90 | |||
| 6ac05e1a10 | |||
| 9768a37252 | |||
| 98c76a7d88 | |||
| 49e2ca1774 | |||
| e9fb1c5ee0 | |||
| 57abe57bc7 | |||
| 9022fa7d93 | |||
| d1621ecb36 | |||
| 6e417312f9 | |||
| 918136aae2 | |||
| 0521d91240 | |||
| c18a223759 | |||
| 298c46de7c | |||
| 2d22fd6e04 | |||
| ef323c291f | |||
| 4eb0fbc22b | |||
| afe22949c5 | |||
| ebb87b286f | |||
| f1da3a44de | |||
| 419dbf0185 | |||
| 909ae6f092 | |||
| a97ff2dc38 | |||
| 7a862a202a | |||
| 1ae3188d34 | |||
| fb7811c469 | |||
| 0a6d730ca0 | |||
| d2d3bee975 | |||
| 78de068cd1 | |||
| 1965dc2c9e | |||
| f0d635ef21 | |||
| d59d667796 | |||
| 5c0e40db7e | |||
| dc9a980958 | |||
| c40653b2b7 | |||
| f240d32ce6 | |||
| 4775e35b3c | |||
| a7535d460d | |||
| 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 | |||
| 2d472892d6 | |||
| 884ca4b96d | |||
| df0409d7f6 | |||
| 5077f557f4 | |||
| 1722d65d22 | |||
| 14023e65d5 | |||
| 237b1a4242 | |||
| ace0279bd0 |
75
.gitea/workflows/deploy-socialize.yml
Normal file
75
.gitea/workflows/deploy-socialize.yml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
name: deploy-socialize
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- 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=/ \
|
||||||
|
-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: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- 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 }}
|
||||||
|
SOCIALIZE_IMAGE_TAG: ${{ gitea.sha }}
|
||||||
|
run: |
|
||||||
|
: "${SOCIALIZE_IMAGE_TAG:?SOCIALIZE_IMAGE_TAG is required}"
|
||||||
|
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
printf '%s' "$DEPLOY_SSH_PRIVATE_KEY_B64" | base64 -d > ~/.ssh/deploy_key
|
||||||
|
chmod 600 ~/.ssh/deploy_key
|
||||||
|
|
||||||
|
write_env_value() {
|
||||||
|
key="$1"
|
||||||
|
value="$2"
|
||||||
|
escaped_value="$(printf '%s' "$value" | sed "s/'/'\\\\''/g")"
|
||||||
|
printf "%s='%s'\n" "$key" "$escaped_value"
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_env="$(mktemp)"
|
||||||
|
{
|
||||||
|
write_env_value SOCIALIZE_IMAGE_TAG "$SOCIALIZE_IMAGE_TAG"
|
||||||
|
} > "$deploy_env"
|
||||||
|
|
||||||
|
scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new "$deploy_env" "$DEPLOY_USER@$DEPLOY_HOST:/srv/prod/socialize/.deploy.env"
|
||||||
|
rm -f "$deploy_env"
|
||||||
|
scp -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new deploy/compose.yml "$DEPLOY_USER@$DEPLOY_HOST:/srv/prod/socialize/compose.yml"
|
||||||
|
|
||||||
|
ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=accept-new "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||||
|
'test -r /etc/socialize/socialize.env && 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 }}
|
|
||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -22,6 +22,10 @@ Thumbs.db
|
|||||||
# .NET
|
# .NET
|
||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
|
**/[Bb]in/
|
||||||
|
**/[Oo]bj/
|
||||||
|
**/[Bb]in[\\]*
|
||||||
|
**/[Oo]bj[\\]*
|
||||||
TestResults/
|
TestResults/
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
@@ -30,6 +34,7 @@ dist/
|
|||||||
.vite/
|
.vite/
|
||||||
|
|
||||||
# Local environment files
|
# Local environment files
|
||||||
|
.env
|
||||||
*.local
|
*.local
|
||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
@@ -38,5 +43,11 @@ App_Data/
|
|||||||
# Local SSL certificates
|
# Local SSL certificates
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
# Ai
|
# AI agent local state
|
||||||
|
.agents
|
||||||
|
.agents/
|
||||||
.codex
|
.codex
|
||||||
|
.codex/
|
||||||
|
|
||||||
|
# Generated local artifacts
|
||||||
|
.artifacts/
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
61
README.md
61
README.md
@@ -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.
|
||||||
|
|
||||||
@@ -76,6 +76,47 @@ http://localhost:8080
|
|||||||
http://<this-machine-lan-ip>:8080
|
http://<this-machine-lan-ip>:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For preprod deployment, configure the `POSTGRES_PASSWORD`, `RESEND_API_KEY`,
|
||||||
|
`RESEND_FROM_EMAIL`, and `JWT_SIGNING_KEY` Gitea secrets.
|
||||||
|
The deploy workflow writes the remote `.env` file and syncs `deploy/compose.yml`
|
||||||
|
before running the server deploy script.
|
||||||
|
Use the raw Resend API key value for `RESEND_API_KEY`, without a `Bearer ` prefix.
|
||||||
|
|
||||||
|
## Preprod Observability
|
||||||
|
|
||||||
|
The optional observability overlay runs a self-hosted Grafana stack for preproduction:
|
||||||
|
|
||||||
|
- Grafana `13.0.1`: dashboards
|
||||||
|
- Prometheus `v3.11.3`: metrics and local alert rules
|
||||||
|
- Loki `3.7.1`: Docker/container logs
|
||||||
|
- Tempo `2.10.3`: traces
|
||||||
|
- Grafana Alloy `v1.16.0`: OTLP receiver and Docker log collector
|
||||||
|
|
||||||
|
Start the app with observability:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/compose.yml -f deploy/observability/compose.observability.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Grafana is exposed at:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
http://127.0.0.1:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Default credentials are `admin` / `admin` unless `GRAFANA_ADMIN_USER` and
|
||||||
|
`GRAFANA_ADMIN_PASSWORD` are set. Set `GRAFANA_HTTP_BIND=0.0.0.0` only when the
|
||||||
|
preprod network boundary is trusted or protected by a reverse proxy/VPN.
|
||||||
|
|
||||||
|
Set a non-default `GRAFANA_ADMIN_PASSWORD` before exposing Grafana outside the
|
||||||
|
host. Prometheus alert rules are provisioned under
|
||||||
|
`deploy/observability/prometheus/rules/`; notification delivery is intentionally
|
||||||
|
left to the preprod operations environment.
|
||||||
|
|
||||||
|
Set `ALERTMANAGER_WEBHOOK_URL` to route alerts to a private notification endpoint.
|
||||||
|
See `docs/OPERATIONS/observability-runbook.md` for bring-up, alert triage, and
|
||||||
|
the optional protected Caddy configuration for Grafana.
|
||||||
|
|
||||||
## Solution
|
## Solution
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -90,6 +131,24 @@ cd frontend
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Database Diagram
|
||||||
|
|
||||||
|
Start PostgreSQL, then generate a local schema diagram:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/generate-db-diagram.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
The script writes an HTML viewer, SVG, PNG, and Graphviz source under:
|
||||||
|
|
||||||
|
```txt
|
||||||
|
.artifacts/db-diagrams/
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `DATABASE_URL`, `PGPASSWORD`, or `~/.pgpass` to provide local database credentials.
|
||||||
|
When using the repository infrastructure script, the diagram script can read from the
|
||||||
|
running `socialize-postgres` container directly.
|
||||||
|
|
||||||
## Agentic Workflow
|
## Agentic Workflow
|
||||||
|
|
||||||
Start here:
|
Start here:
|
||||||
|
|||||||
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
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Observability;
|
||||||
using Socialize.Api.Infrastructure.Security;
|
using Socialize.Api.Infrastructure.Security;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
@@ -11,7 +12,7 @@ using Microsoft.IdentityModel.Tokens;
|
|||||||
|
|
||||||
namespace Socialize;
|
namespace Socialize;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class ApplicationRegistration
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddWebServices(this IServiceCollection services)
|
public static IServiceCollection AddWebServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
@@ -20,7 +21,10 @@ public static class DependencyInjection
|
|||||||
services.AddHttpContextAccessor();
|
services.AddHttpContextAccessor();
|
||||||
|
|
||||||
services.AddHealthChecks()
|
services.AddHealthChecks()
|
||||||
.AddDbContextCheck<AppDbContext>();
|
.AddCheck("self", () => Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult.Healthy(), tags: ["live"])
|
||||||
|
.AddDbContextCheck<AppDbContext>("postgres", tags: ["ready"])
|
||||||
|
.AddCheck<LocalBlobStorageHealthCheck>("local_blob_storage", tags: ["ready"])
|
||||||
|
.AddCheck<EmailerConfigurationHealthCheck>("emailer_configuration", tags: ["ready"]);
|
||||||
|
|
||||||
services.AddHttpClient();
|
services.AddHttpClient();
|
||||||
services.AddScoped<AccessScopeService>();
|
services.AddScoped<AccessScopeService>();
|
||||||
@@ -70,7 +74,6 @@ public static class DependencyInjection
|
|||||||
{
|
{
|
||||||
authenticationBuilder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
|
authenticationBuilder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtBearerOptions =>
|
||||||
{
|
{
|
||||||
jwtBearerOptions.Authority = "https://hutopy.com";
|
|
||||||
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
|
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
ValidateIssuer = true,
|
ValidateIssuer = true,
|
||||||
@@ -79,7 +82,7 @@ public static class DependencyInjection
|
|||||||
ValidAudience = authJwt["Audience"],
|
ValidAudience = authJwt["Audience"],
|
||||||
ValidateLifetime = true,
|
ValidateLifetime = true,
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authJwt["Key"] ??
|
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authJwt["Key"] ??
|
||||||
throw new ArgumentNullException("The Jwt Key is missing.")))
|
throw new InvalidOperationException("Authentication:Jwt:Key is required.")))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -90,9 +93,9 @@ public static class DependencyInjection
|
|||||||
authenticationBuilder.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
|
authenticationBuilder.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
|
||||||
{
|
{
|
||||||
options.ClientId = authGoogle["ClientId"] ??
|
options.ClientId = authGoogle["ClientId"] ??
|
||||||
throw new ArgumentNullException("The Google ClientId is missing.");
|
throw new InvalidOperationException("Authentication:Google:ClientId is required.");
|
||||||
options.ClientSecret = authGoogle["ClientSecret"] ??
|
options.ClientSecret = authGoogle["ClientSecret"] ??
|
||||||
throw new ArgumentNullException("The Google ClientSecret is missing.");
|
throw new InvalidOperationException("Authentication:Google:ClientSecret is required.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,9 +105,9 @@ public static class DependencyInjection
|
|||||||
authenticationBuilder.AddFacebook(FacebookDefaults.AuthenticationScheme, options =>
|
authenticationBuilder.AddFacebook(FacebookDefaults.AuthenticationScheme, options =>
|
||||||
{
|
{
|
||||||
options.ClientId = authFacebook["ClientId"] ??
|
options.ClientId = authFacebook["ClientId"] ??
|
||||||
throw new ArgumentNullException("The Facebook ClientId is missing.");
|
throw new InvalidOperationException("Authentication:Facebook:ClientId is required.");
|
||||||
options.ClientSecret = authFacebook["ClientSecret"] ??
|
options.ClientSecret = authFacebook["ClientSecret"] ??
|
||||||
throw new ArgumentNullException("The Facebook ClientSecret is missing.");
|
throw new InvalidOperationException("Authentication:Facebook:ClientSecret is required.");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Common.Domain;
|
namespace Socialize.Api.Common.Domain;
|
||||||
|
|
||||||
public abstract class Entity
|
internal abstract class Entity
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid CreatedBy { get; init; }
|
public Guid CreatedBy { get; init; }
|
||||||
|
|||||||
@@ -2,51 +2,76 @@ 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;
|
||||||
using Socialize.Api.Modules.Feedback.Data;
|
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.Projects.Data;
|
using Socialize.Api.Modules.Campaigns.Data;
|
||||||
|
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
using Socialize.Api.Modules.Organizations.Data;
|
||||||
|
using Socialize.Api.Modules.ReleaseCommunications.Data;
|
||||||
using Socialize.Api.Modules.Workspaces.Data;
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Data;
|
namespace Socialize.Api.Data;
|
||||||
|
|
||||||
public class AppDbContext(
|
internal class AppDbContext(
|
||||||
DbContextOptions<AppDbContext> options)
|
DbContextOptions<AppDbContext> options)
|
||||||
: IdentityDbContext<User, Role, Guid>(options)
|
: IdentityDbContext<User, Role, Guid>(options)
|
||||||
{
|
{
|
||||||
|
public DbSet<Organization> Organizations => Set<Organization>();
|
||||||
|
public DbSet<OrganizationMembershipTier> OrganizationMembershipTiers => Set<OrganizationMembershipTier>();
|
||||||
|
public DbSet<OrganizationMembershipTierTranslation> OrganizationMembershipTierTranslations =>
|
||||||
|
Set<OrganizationMembershipTierTranslation>();
|
||||||
|
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<Project> Projects => Set<Project>();
|
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>();
|
||||||
|
public DbSet<ApprovalWorkflowInstance> ApprovalWorkflowInstances => Set<ApprovalWorkflowInstance>();
|
||||||
public DbSet<ApprovalRequest> ApprovalRequests => Set<ApprovalRequest>();
|
public DbSet<ApprovalRequest> ApprovalRequests => Set<ApprovalRequest>();
|
||||||
public DbSet<ApprovalDecision> ApprovalDecisions => Set<ApprovalDecision>();
|
public DbSet<ApprovalDecision> ApprovalDecisions => Set<ApprovalDecision>();
|
||||||
|
public DbSet<WorkspaceApprovalStepConfiguration> WorkspaceApprovalStepConfigurations => Set<WorkspaceApprovalStepConfiguration>();
|
||||||
public DbSet<NotificationEvent> NotificationEvents => Set<NotificationEvent>();
|
public DbSet<NotificationEvent> NotificationEvents => Set<NotificationEvent>();
|
||||||
public DbSet<FeedbackReport> FeedbackReports => Set<FeedbackReport>();
|
public DbSet<FeedbackReport> FeedbackReports => Set<FeedbackReport>();
|
||||||
public DbSet<FeedbackTag> FeedbackTags => Set<FeedbackTag>();
|
public DbSet<FeedbackTag> FeedbackTags => Set<FeedbackTag>();
|
||||||
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>();
|
||||||
|
public DbSet<ReleaseUpdate> ReleaseUpdates => Set<ReleaseUpdate>();
|
||||||
|
public DbSet<ReleaseUpdateReadReceipt> ReleaseUpdateReadReceipts => Set<ReleaseUpdateReadReceipt>();
|
||||||
|
public DbSet<ReleaseCommit> ReleaseCommits => Set<ReleaseCommit>();
|
||||||
|
public DbSet<ReleaseUpdateEmailDigestReceipt> ReleaseUpdateEmailDigestReceipts => Set<ReleaseUpdateEmailDigestReceipt>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(builder);
|
base.OnModelCreating(builder);
|
||||||
|
|
||||||
|
builder.ConfigureOrganizationsModule();
|
||||||
builder.ConfigureWorkspacesModule();
|
builder.ConfigureWorkspacesModule();
|
||||||
|
builder.ConfigureChannelsModule();
|
||||||
builder.ConfigureClientsModule();
|
builder.ConfigureClientsModule();
|
||||||
builder.ConfigureProjectsModule();
|
builder.ConfigureCampaignsModule();
|
||||||
builder.ConfigureContentItemsModule();
|
builder.ConfigureContentItemsModule();
|
||||||
builder.ConfigureAssetsModule();
|
builder.ConfigureAssetsModule();
|
||||||
builder.ConfigureCommentsModule();
|
builder.ConfigureCommentsModule();
|
||||||
builder.ConfigureApprovalsModule();
|
builder.ConfigureApprovalsModule();
|
||||||
builder.ConfigureNotificationsModule();
|
builder.ConfigureNotificationsModule();
|
||||||
builder.ConfigureFeedbackModule();
|
builder.ConfigureFeedbackModule();
|
||||||
|
builder.ConfigureCalendarIntegrationsModule();
|
||||||
|
builder.ConfigureReleaseCommunicationsModule();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<wpf:ResourceDictionary xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:s="clr-namespace:System;assembly=mscorlib"
|
|
||||||
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xml:space="preserve">
|
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=hutopy/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
||||||
|
|
||||||
public sealed class LocalBlobStorageOptions
|
internal sealed class LocalBlobStorageOptions
|
||||||
{
|
{
|
||||||
public const string SectionName = "LocalBlobStorage";
|
public const string SectionName = "LocalBlobStorage";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class CommonFileNames
|
internal static class CommonFileNames
|
||||||
{
|
{
|
||||||
public const string ProfilePicture = "profilePicture";
|
public const string ProfilePicture = "profilePicture";
|
||||||
public const string LogoPicture = "logoPicture";
|
public const string LogoPicture = "logoPicture";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class ContentTypes
|
internal static class ContentTypes
|
||||||
{
|
{
|
||||||
private const string ImagePng = "image/png";
|
private const string ImagePng = "image/png";
|
||||||
private const string ImageJpeg = "image/jpeg";
|
private const string ImageJpeg = "image/jpeg";
|
||||||
@@ -39,6 +39,6 @@ public static class ContentTypes
|
|||||||
|
|
||||||
// Check for HTML content by looking for "<!DOCTYPE html>" or "<html>" tags
|
// Check for HTML content by looking for "<!DOCTYPE html>" or "<html>" tags
|
||||||
string content = Encoding.UTF8.GetString(buffer);
|
string content = Encoding.UTF8.GetString(buffer);
|
||||||
return content.Contains("<!DOCTYPE html>");
|
return content.Contains("<!DOCTYPE html>", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public interface IBlobStorage
|
internal interface IBlobStorage
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Upload a file to blob storage.
|
/// Upload a file to blob storage.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
|
||||||
public static class SubDirectoryNames
|
internal static class SubDirectoryNames
|
||||||
{
|
{
|
||||||
public const string Profile = "profile";
|
public const string Profile = "profile";
|
||||||
public const string Contents = "contents";
|
public const string Contents = "contents";
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
using Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
||||||
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
using Socialize.Api.Infrastructure.BlobStorage.Contracts;
|
||||||
|
using Socialize.Api.Infrastructure.Observability;
|
||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.BlobStorage.Services;
|
namespace Socialize.Api.Infrastructure.BlobStorage.Services;
|
||||||
|
|
||||||
public sealed class LocalBlobStorage(
|
internal sealed class LocalBlobStorage(
|
||||||
IWebHostEnvironment environment,
|
IWebHostEnvironment environment,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
IOptions<LocalBlobStorageOptions> options,
|
IOptions<LocalBlobStorageOptions> options,
|
||||||
ILogger<LocalBlobStorage> logger)
|
ILogger<LocalBlobStorage> logger,
|
||||||
|
SocializeMetrics metrics)
|
||||||
: IBlobStorage
|
: IBlobStorage
|
||||||
{
|
{
|
||||||
private const long MaxUploadSize = 10 * 1024 * 1024;
|
private const long MaxUploadSize = 10 * 1024 * 1024;
|
||||||
private const string ContentTypeMetadataSuffix = ".content-type";
|
private const string ContentTypeMetadataSuffix = ".content-type";
|
||||||
|
|
||||||
|
private static readonly char[] PathSeparators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, string, string, string, string, Exception?> LogUploadedFile =
|
||||||
|
LoggerMessage.Define<string, string, string, string>(
|
||||||
|
LogLevel.Information,
|
||||||
|
new EventId(1, nameof(UploadFileAsync)),
|
||||||
|
"Blob storage: Uploaded [{BlobName}] to local container [{ContainerName}] with contentType [{ContentType}] and uri [{FileUri}]");
|
||||||
|
|
||||||
private readonly LocalBlobStorageOptions _options = options.Value;
|
private readonly LocalBlobStorageOptions _options = options.Value;
|
||||||
|
|
||||||
public async Task<string> UploadFileAsync(
|
public async Task<string> UploadFileAsync(
|
||||||
@@ -23,37 +33,51 @@ public sealed class LocalBlobStorage(
|
|||||||
string contentType,
|
string contentType,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
stream.Position = 0;
|
try
|
||||||
|
|
||||||
if (stream.Length > MaxUploadSize)
|
|
||||||
{
|
{
|
||||||
logger.LogError("Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.", MaxUploadSize);
|
stream.Position = 0;
|
||||||
throw new InvalidOperationException($"Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ContentTypes.IsAllowed(contentType, stream))
|
if (stream.Length > MaxUploadSize)
|
||||||
|
{
|
||||||
|
logger.LogError("Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.", MaxUploadSize);
|
||||||
|
throw new InvalidOperationException($"Blob storage: File size exceeds the maximum allowed size of {MaxUploadSize} bytes.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ContentTypes.IsAllowed(contentType, stream))
|
||||||
|
{
|
||||||
|
logger.LogError("Blob storage: Unsupported file type {ContentType}.", contentType);
|
||||||
|
throw new InvalidOperationException("Unsupported file type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string relativePath = GetSafeRelativePath(containerName, blobName);
|
||||||
|
string filePath = Path.Combine(GetRootPath(), relativePath);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? GetRootPath());
|
||||||
|
|
||||||
|
await using FileStream fileStream = File.Create(filePath);
|
||||||
|
await stream.CopyToAsync(fileStream, ct);
|
||||||
|
await File.WriteAllTextAsync(GetContentTypeMetadataPath(filePath), contentType, ct);
|
||||||
|
|
||||||
|
string fileUri = BuildPublicUrl(relativePath);
|
||||||
|
LogUploadedFile(logger, blobName, containerName, contentType, fileUri, null);
|
||||||
|
metrics.RecordBlobStorageOperation("upload", true);
|
||||||
|
|
||||||
|
return fileUri;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
{
|
{
|
||||||
logger.LogError("Blob storage: Unsupported file type {ContentType}.", contentType);
|
metrics.RecordBlobStorageOperation("upload", false);
|
||||||
throw new InvalidOperationException("Unsupported file type.");
|
throw;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
metrics.RecordBlobStorageOperation("upload", false);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
metrics.RecordBlobStorageOperation("upload", false);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
string relativePath = GetSafeRelativePath(containerName, blobName);
|
|
||||||
string filePath = Path.Combine(GetRootPath(), relativePath);
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(filePath) ?? GetRootPath());
|
|
||||||
|
|
||||||
await using FileStream fileStream = File.Create(filePath);
|
|
||||||
await stream.CopyToAsync(fileStream, ct);
|
|
||||||
await File.WriteAllTextAsync(GetContentTypeMetadataPath(filePath), contentType, ct);
|
|
||||||
|
|
||||||
string fileUri = BuildPublicUrl(relativePath);
|
|
||||||
logger.LogInformation(
|
|
||||||
"Blob storage: Uploaded [{BlobName}] to local container [{ContainerName}] with contentType [{ContentType}] and uri [{FileUri}]",
|
|
||||||
blobName,
|
|
||||||
containerName,
|
|
||||||
contentType,
|
|
||||||
fileUri);
|
|
||||||
|
|
||||||
return fileUri;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MemoryStream> DownloadFileAsync(
|
public async Task<MemoryStream> DownloadFileAsync(
|
||||||
@@ -61,19 +85,43 @@ public sealed class LocalBlobStorage(
|
|||||||
string blobName,
|
string blobName,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string filePath = Path.Combine(GetRootPath(), GetSafeRelativePath(containerName, blobName));
|
try
|
||||||
|
|
||||||
if (!File.Exists(filePath))
|
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException("Blob storage: Local file was not found.", blobName);
|
string filePath = Path.Combine(GetRootPath(), GetSafeRelativePath(containerName, blobName));
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
throw new FileNotFoundException("Blob storage: Local file was not found.", blobName);
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryStream memoryStream = new();
|
||||||
|
await using FileStream fileStream = File.OpenRead(filePath);
|
||||||
|
await fileStream.CopyToAsync(memoryStream, ct);
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
metrics.RecordBlobStorageOperation("download", true);
|
||||||
|
|
||||||
|
return memoryStream;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
metrics.RecordBlobStorageOperation("download", false);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
metrics.RecordBlobStorageOperation("download", false);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
metrics.RecordBlobStorageOperation("download", false);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
metrics.RecordBlobStorageOperation("download", false);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
MemoryStream memoryStream = new();
|
|
||||||
await using FileStream fileStream = File.OpenRead(filePath);
|
|
||||||
await fileStream.CopyToAsync(memoryStream, ct);
|
|
||||||
memoryStream.Position = 0;
|
|
||||||
|
|
||||||
return memoryStream;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal string GetRootPath()
|
internal string GetRootPath()
|
||||||
@@ -106,7 +154,7 @@ public sealed class LocalBlobStorage(
|
|||||||
throw new InvalidOperationException("Blob storage: Blob paths must be relative.");
|
throw new InvalidOperationException("Blob storage: Blob paths must be relative.");
|
||||||
}
|
}
|
||||||
|
|
||||||
string[] pathParts = [containerName, .. blobName.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar])];
|
string[] pathParts = [containerName, .. blobName.Split(PathSeparators)];
|
||||||
if (pathParts.Any(part => part is "" or "." or ".."))
|
if (pathParts.Any(part => part is "" or "." or ".."))
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Blob storage: Blob paths must not contain relative path segments.");
|
throw new InvalidOperationException("Blob storage: Blob paths must not contain relative path segments.");
|
||||||
@@ -135,7 +183,7 @@ public sealed class LocalBlobStorage(
|
|||||||
? "/api/storage"
|
? "/api/storage"
|
||||||
: requestPath.Trim();
|
: requestPath.Trim();
|
||||||
|
|
||||||
return normalized.StartsWith("/", StringComparison.Ordinal)
|
return normalized.StartsWith('/')
|
||||||
? normalized.TrimEnd('/')
|
? normalized.TrimEnd('/')
|
||||||
: $"/{normalized.TrimEnd('/')}";
|
: $"/{normalized.TrimEnd('/')}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Configuration;
|
namespace Socialize.Api.Infrastructure.Configuration;
|
||||||
|
|
||||||
public class WebsiteOptions
|
internal class WebsiteOptions
|
||||||
{
|
{
|
||||||
public const string SectionName = "Website";
|
public const string SectionName = "Website";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Development;
|
|
||||||
|
|
||||||
public record DevelopmentSeedOptions
|
|
||||||
{
|
|
||||||
public const string SectionName = "DevelopmentSeed";
|
|
||||||
|
|
||||||
public bool Enabled { get; init; } = true;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Emailer.Configuration;
|
namespace Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||||
|
|
||||||
public class EmailerOptions
|
internal class EmailerOptions
|
||||||
{
|
{
|
||||||
public const string ConfigurationSection = "Emailer";
|
public const string ConfigurationSection = "Emailer";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Emailer.Contracts;
|
namespace Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||||
|
|
||||||
public interface IEmailSender
|
internal interface IEmailSender
|
||||||
{
|
{
|
||||||
Task SendEmailAsync(string email, string subject, string message);
|
Task SendEmailAsync(string email, string subject, string message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||||
|
using Socialize.Api.Infrastructure.Observability;
|
||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
public class LoggerEmailSender(ILogger<IEmailSender> logger)
|
internal class LoggerEmailSender(
|
||||||
|
ILogger<IEmailSender> logger,
|
||||||
|
SocializeMetrics metrics)
|
||||||
: IEmailSender
|
: IEmailSender
|
||||||
{
|
{
|
||||||
public async Task SendEmailAsync(string email, string subject, string message)
|
private static readonly Action<ILogger, string, string, string, string, Exception?> LogDevelopmentEmail =
|
||||||
|
LoggerMessage.Define<string, string, string, string>(
|
||||||
|
LogLevel.Information,
|
||||||
|
new EventId(1, nameof(SendEmailAsync)),
|
||||||
|
"Development email to {Email} with subject {Subject}:{NewLine}{Message}");
|
||||||
|
|
||||||
|
public Task SendEmailAsync(string email, string subject, string message)
|
||||||
{
|
{
|
||||||
try
|
LogDevelopmentEmail(logger, email, subject, Environment.NewLine, message, null);
|
||||||
{
|
metrics.RecordEmailDelivery("logger", true);
|
||||||
logger.LogInformation("Sending email to {Email} with subject: {Subject}", email, subject);
|
|
||||||
await Task.Delay(1000);
|
return Task.CompletedTask;
|
||||||
logger.LogInformation("Email sent successfully to {Email}", email);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Failed to send email to {Email}", email);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using PostmarkDotNet;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
public class PostmarkEmailSender : IEmailSender
|
internal class PostmarkEmailSender : IEmailSender
|
||||||
{
|
{
|
||||||
private readonly PostmarkClient _client;
|
private readonly PostmarkClient _client;
|
||||||
private readonly EmailerOptions _options;
|
private readonly EmailerOptions _options;
|
||||||
|
|||||||
@@ -3,44 +3,95 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Socialize.Api.Infrastructure.Emailer.Configuration;
|
using Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||||
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
using Socialize.Api.Infrastructure.Emailer.Contracts;
|
||||||
|
using Socialize.Api.Infrastructure.Observability;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
namespace Socialize.Api.Infrastructure.Emailer.Services;
|
||||||
|
|
||||||
public class ResendEmailSender : IEmailSender
|
internal class ResendEmailSender : IEmailSender
|
||||||
{
|
{
|
||||||
private static readonly Uri EndpointUri = new("https://api.resend.com/emails");
|
private static readonly Uri EndpointUri = new("https://api.resend.com/emails");
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly SocializeMetrics _metrics;
|
||||||
private readonly EmailerOptions _options;
|
private readonly EmailerOptions _options;
|
||||||
|
|
||||||
public ResendEmailSender(
|
public ResendEmailSender(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IOptions<EmailerOptions> options)
|
IOptions<EmailerOptions> options,
|
||||||
|
SocializeMetrics metrics)
|
||||||
{
|
{
|
||||||
_httpClient = httpClientFactory.CreateClient();
|
_httpClient = httpClientFactory.CreateClient();
|
||||||
|
_metrics = metrics;
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
|
|
||||||
|
string apiKey = NormalizeApiKey(_options.ApiKey);
|
||||||
|
string fromEmail = _options.FromEmail?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(apiKey))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Emailer:ApiKey is required when using Resend email delivery.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(fromEmail))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Emailer:FromEmail is required when using Resend email delivery.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_options.ApiKey = apiKey;
|
||||||
|
_options.FromEmail = fromEmail;
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Authorization =
|
_httpClient.DefaultRequestHeaders.Authorization =
|
||||||
new AuthenticationHeaderValue("Bearer", _options.ApiKey);
|
new AuthenticationHeaderValue("Bearer", apiKey);
|
||||||
|
|
||||||
_httpClient.DefaultRequestHeaders.Accept.Add(
|
_httpClient.DefaultRequestHeaders.Accept.Add(
|
||||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
new MediaTypeWithQualityHeaderValue("application/json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SendEmailAsync(string toEmail, string subject, string htmlMessage)
|
public async Task SendEmailAsync(string email, string subject, string message)
|
||||||
{
|
{
|
||||||
var payload = new { from = _options.FromEmail, to = toEmail, subject, html = htmlMessage };
|
var payload = new { from = _options.FromEmail, to = email, subject, html = message };
|
||||||
|
|
||||||
string json = JsonSerializer.Serialize(payload);
|
string json = JsonSerializer.Serialize(payload);
|
||||||
StringContent content = new(json, Encoding.UTF8, "application/json");
|
using StringContent content = new(json, Encoding.UTF8, "application/json");
|
||||||
|
try
|
||||||
HttpResponseMessage response = await _httpClient.PostAsync(EndpointUri, content);
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
string body = await response.Content.ReadAsStringAsync();
|
using HttpResponseMessage response = await _httpClient.PostAsync(EndpointUri, content);
|
||||||
throw new InvalidOperationException(
|
|
||||||
$"Resend email failed: {response.StatusCode} - {body}");
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
string body = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Resend email failed: {response.StatusCode} - {body}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_metrics.RecordEmailDelivery("resend", true);
|
||||||
|
}
|
||||||
|
catch (HttpRequestException)
|
||||||
|
{
|
||||||
|
_metrics.RecordEmailDelivery("resend", false);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
_metrics.RecordEmailDelivery("resend", false);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
_metrics.RecordEmailDelivery("resend", false);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeApiKey(string? apiKey)
|
||||||
|
{
|
||||||
|
string normalized = apiKey?.Trim().Trim('"', '\'') ?? string.Empty;
|
||||||
|
const string bearerPrefix = "Bearer ";
|
||||||
|
if (normalized.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
normalized = normalized[bearerPrefix.Length..].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ using Socialize.Api.Infrastructure.Payments.Stripe.Configuration;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure;
|
namespace Socialize.Api.Infrastructure;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class InfrastructureRegistration
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder AddInfrastructureModule(
|
public static WebApplicationBuilder AddInfrastructureModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
@@ -26,8 +26,14 @@ public static class DependencyInjection
|
|||||||
|
|
||||||
builder.Services.Configure<EmailerOptions>(
|
builder.Services.Configure<EmailerOptions>(
|
||||||
builder.Configuration.GetSection(EmailerOptions.ConfigurationSection));
|
builder.Configuration.GetSection(EmailerOptions.ConfigurationSection));
|
||||||
builder.Services.AddTransient<IEmailSender, ResendEmailSender>();
|
if (builder.Environment.IsDevelopment())
|
||||||
//builder.Services.AddTransient<IEmailSender, EmailSender>();
|
{
|
||||||
|
builder.Services.AddTransient<IEmailSender, LoggerEmailSender>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder.Services.AddTransient<IEmailSender, ResendEmailSender>();
|
||||||
|
}
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Socialize.Api.Infrastructure.Emailer.Configuration;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Infrastructure.Observability;
|
||||||
|
|
||||||
|
internal sealed class EmailerConfigurationHealthCheck(
|
||||||
|
IWebHostEnvironment environment,
|
||||||
|
IOptions<EmailerOptions> options)
|
||||||
|
: IHealthCheck
|
||||||
|
{
|
||||||
|
public Task<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
return Task.FromResult(HealthCheckResult.Healthy("Development email sender logs email instead of delivering it."));
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailerOptions value = options.Value;
|
||||||
|
if (string.IsNullOrWhiteSpace(value.ApiKey) || string.IsNullOrWhiteSpace(value.FromEmail))
|
||||||
|
{
|
||||||
|
return Task.FromResult(HealthCheckResult.Unhealthy("Emailer API key or from address is missing."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(HealthCheckResult.Healthy("Emailer configuration is present."));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Socialize.Api.Infrastructure.BlobStorage.Configuration;
|
||||||
|
using Socialize.Api.Infrastructure.BlobStorage.Services;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Infrastructure.Observability;
|
||||||
|
|
||||||
|
internal sealed class LocalBlobStorageHealthCheck(
|
||||||
|
LocalBlobStorage blobStorage,
|
||||||
|
IOptions<LocalBlobStorageOptions> options)
|
||||||
|
: IHealthCheck
|
||||||
|
{
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
string rootPath = blobStorage.GetRootPath();
|
||||||
|
if (string.IsNullOrWhiteSpace(options.Value.RequestPath))
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Unhealthy("Local blob storage request path is not configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(rootPath);
|
||||||
|
string probePath = Path.Combine(rootPath, ".healthcheck");
|
||||||
|
await File.WriteAllTextAsync(
|
||||||
|
probePath,
|
||||||
|
DateTimeOffset.UtcNow.ToString("O", System.Globalization.CultureInfo.InvariantCulture),
|
||||||
|
cancellationToken);
|
||||||
|
File.Delete(probePath);
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy("Local blob storage is writable.");
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Unhealthy("Local blob storage is not writable.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using Npgsql;
|
||||||
|
using OpenTelemetry.Logs;
|
||||||
|
using OpenTelemetry.Metrics;
|
||||||
|
using OpenTelemetry.Resources;
|
||||||
|
using OpenTelemetry.Trace;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Infrastructure.Observability;
|
||||||
|
|
||||||
|
internal static class ObservabilityRegistration
|
||||||
|
{
|
||||||
|
private const string DefaultServiceName = "socialize-api";
|
||||||
|
|
||||||
|
public static WebApplicationBuilder AddObservability(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
string serviceName = GetConfigurationValue(builder.Configuration, "OTEL_SERVICE_NAME", DefaultServiceName);
|
||||||
|
string serviceVersion = typeof(Program).Assembly.GetName().Version?.ToString() ?? "unknown";
|
||||||
|
|
||||||
|
builder.Logging.Configure(options =>
|
||||||
|
{
|
||||||
|
options.ActivityTrackingOptions =
|
||||||
|
ActivityTrackingOptions.TraceId |
|
||||||
|
ActivityTrackingOptions.SpanId |
|
||||||
|
ActivityTrackingOptions.ParentId;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Logging.AddJsonConsole(options =>
|
||||||
|
{
|
||||||
|
options.IncludeScopes = true;
|
||||||
|
options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ";
|
||||||
|
options.UseUtcTimestamp = true;
|
||||||
|
options.JsonWriterOptions = new JsonWriterOptions { Indented = false };
|
||||||
|
});
|
||||||
|
|
||||||
|
bool otlpEnabled = HasOtlpEndpoint(builder.Configuration);
|
||||||
|
if (otlpEnabled)
|
||||||
|
{
|
||||||
|
builder.Logging.AddOpenTelemetry(options =>
|
||||||
|
{
|
||||||
|
options.IncludeFormattedMessage = true;
|
||||||
|
options.IncludeScopes = true;
|
||||||
|
options.ParseStateValues = true;
|
||||||
|
options.SetResourceBuilder(BuildResource(serviceName, serviceVersion));
|
||||||
|
options.AddOtlpExporter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<SocializeMetrics>();
|
||||||
|
builder.Services.AddHostedService<WorkflowHealthSamplerService>();
|
||||||
|
builder.Services
|
||||||
|
.AddOpenTelemetry()
|
||||||
|
.ConfigureResource(resource => resource.AddService(
|
||||||
|
serviceName,
|
||||||
|
serviceVersion: serviceVersion))
|
||||||
|
.WithTracing(tracing =>
|
||||||
|
{
|
||||||
|
tracing
|
||||||
|
.AddSource(SocializeMetrics.ActivitySourceName)
|
||||||
|
.AddAspNetCoreInstrumentation(options =>
|
||||||
|
{
|
||||||
|
options.RecordException = true;
|
||||||
|
})
|
||||||
|
.AddHttpClientInstrumentation()
|
||||||
|
.AddNpgsql();
|
||||||
|
|
||||||
|
if (otlpEnabled)
|
||||||
|
{
|
||||||
|
tracing.AddOtlpExporter();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.WithMetrics(metrics =>
|
||||||
|
{
|
||||||
|
metrics
|
||||||
|
.AddMeter(SocializeMetrics.MeterName)
|
||||||
|
.AddAspNetCoreInstrumentation()
|
||||||
|
.AddHttpClientInstrumentation()
|
||||||
|
.AddRuntimeInstrumentation();
|
||||||
|
|
||||||
|
if (otlpEnabled)
|
||||||
|
{
|
||||||
|
metrics.AddOtlpExporter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IApplicationBuilder UseObservabilityLoggingScope(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
return app.UseMiddleware<RequestLoggingScopeMiddleware>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEndpointRouteBuilder MapObservabilityHealthChecks(this IEndpointRouteBuilder endpoints)
|
||||||
|
{
|
||||||
|
endpoints.MapHealthChecks(
|
||||||
|
"/health",
|
||||||
|
new HealthCheckOptions { ResponseWriter = WriteHealthResponseAsync });
|
||||||
|
endpoints.MapHealthChecks(
|
||||||
|
"/health/live",
|
||||||
|
new HealthCheckOptions
|
||||||
|
{
|
||||||
|
Predicate = registration => registration.Tags.Contains("live", StringComparer.Ordinal),
|
||||||
|
ResponseWriter = WriteHealthResponseAsync,
|
||||||
|
});
|
||||||
|
endpoints.MapHealthChecks(
|
||||||
|
"/health/ready",
|
||||||
|
new HealthCheckOptions
|
||||||
|
{
|
||||||
|
Predicate = registration => registration.Tags.Contains("ready", StringComparer.Ordinal),
|
||||||
|
ResponseWriter = WriteHealthResponseAsync,
|
||||||
|
});
|
||||||
|
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceBuilder BuildResource(string serviceName, string serviceVersion)
|
||||||
|
{
|
||||||
|
return ResourceBuilder.CreateDefault().AddService(
|
||||||
|
serviceName,
|
||||||
|
serviceVersion: serviceVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasOtlpEndpoint(ConfigurationManager configuration)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]) ||
|
||||||
|
!string.IsNullOrWhiteSpace(configuration["Otlp:Endpoint"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetConfigurationValue(
|
||||||
|
ConfigurationManager configuration,
|
||||||
|
string key,
|
||||||
|
string fallback)
|
||||||
|
{
|
||||||
|
string? value = configuration[key];
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? fallback : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task WriteHealthResponseAsync(HttpContext context, HealthReport report)
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
status = report.Status.ToString(),
|
||||||
|
checks = report.Entries.Select(entry => new
|
||||||
|
{
|
||||||
|
name = entry.Key,
|
||||||
|
status = entry.Value.Status.ToString(),
|
||||||
|
description = entry.Value.Description,
|
||||||
|
duration = entry.Value.Duration.TotalMilliseconds,
|
||||||
|
}),
|
||||||
|
duration = report.TotalDuration.TotalMilliseconds,
|
||||||
|
};
|
||||||
|
|
||||||
|
await JsonSerializer.SerializeAsync(
|
||||||
|
context.Response.Body,
|
||||||
|
response,
|
||||||
|
cancellationToken: context.RequestAborted);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Infrastructure.Observability;
|
||||||
|
|
||||||
|
internal sealed class RequestLoggingScopeMiddleware(
|
||||||
|
RequestDelegate next,
|
||||||
|
ILogger<RequestLoggingScopeMiddleware> logger)
|
||||||
|
{
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
Dictionary<string, object?> scope = new()
|
||||||
|
{
|
||||||
|
["trace_id"] = Activity.Current?.TraceId.ToString() ?? context.TraceIdentifier,
|
||||||
|
["span_id"] = Activity.Current?.SpanId.ToString(),
|
||||||
|
["http.method"] = context.Request.Method,
|
||||||
|
["url.path"] = context.Request.Path.Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (context.User.Identity?.IsAuthenticated == true)
|
||||||
|
{
|
||||||
|
scope["user.id"] = context.User.GetUserId();
|
||||||
|
scope["user.email"] = context.User.GetEmail();
|
||||||
|
}
|
||||||
|
|
||||||
|
AddGuidIfPresent(scope, "organization.id", context, "organizationId");
|
||||||
|
AddGuidIfPresent(scope, "workspace.id", context, "workspaceId");
|
||||||
|
AddGuidIfPresent(scope, "client.id", context, "clientId");
|
||||||
|
AddGuidIfPresent(scope, "campaign.id", context, "campaignId");
|
||||||
|
AddGuidIfPresent(scope, "content_item.id", context, "contentItemId");
|
||||||
|
|
||||||
|
using IDisposable? _ = logger.BeginScope(scope);
|
||||||
|
await next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddGuidIfPresent(
|
||||||
|
Dictionary<string, object?> scope,
|
||||||
|
string scopeKey,
|
||||||
|
HttpContext context,
|
||||||
|
string requestKey)
|
||||||
|
{
|
||||||
|
string? value = GetRouteOrQueryValue(context, requestKey);
|
||||||
|
if (Guid.TryParse(value, out Guid id))
|
||||||
|
{
|
||||||
|
scope[scopeKey] = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? GetRouteOrQueryValue(HttpContext context, string key)
|
||||||
|
{
|
||||||
|
object? routeValue = context.Request.RouteValues[key];
|
||||||
|
if (routeValue is not null)
|
||||||
|
{
|
||||||
|
return Convert.ToString(routeValue, System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.Request.Query.TryGetValue(key, out Microsoft.Extensions.Primitives.StringValues queryValue)
|
||||||
|
? queryValue.ToString()
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.Metrics;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Infrastructure.Observability;
|
||||||
|
|
||||||
|
internal sealed class SocializeMetrics : IDisposable
|
||||||
|
{
|
||||||
|
public const string MeterName = "Socialize.Api";
|
||||||
|
public const string ActivitySourceName = "Socialize.Api";
|
||||||
|
|
||||||
|
private readonly Counter<long> _approvalDecisionCounter;
|
||||||
|
private readonly Counter<long> _backgroundJobRunCounter;
|
||||||
|
private readonly Counter<long> _blobStorageOperationCounter;
|
||||||
|
private readonly Counter<long> _commentCreatedCounter;
|
||||||
|
private readonly Counter<long> _contentItemCreatedCounter;
|
||||||
|
private readonly Counter<long> _emailDeliveryCounter;
|
||||||
|
private readonly Counter<long> _feedbackSubmittedCounter;
|
||||||
|
private readonly Counter<long> _loginAttemptCounter;
|
||||||
|
private readonly Counter<long> _organizationCreatedCounter;
|
||||||
|
private readonly Counter<long> _workspaceCreatedCounter;
|
||||||
|
private readonly Counter<long> _workspaceInviteCreatedCounter;
|
||||||
|
private readonly object _workflowHealthLock = new();
|
||||||
|
private WorkflowHealthSnapshot _workflowHealthSnapshot = WorkflowHealthSnapshot.Empty;
|
||||||
|
|
||||||
|
public SocializeMetrics()
|
||||||
|
{
|
||||||
|
Meter = new Meter(MeterName);
|
||||||
|
ActivitySource = new ActivitySource(ActivitySourceName);
|
||||||
|
|
||||||
|
_loginAttemptCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.login.attempts",
|
||||||
|
description: "Login attempts partitioned by outcome.");
|
||||||
|
_organizationCreatedCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.organizations.created",
|
||||||
|
description: "Organizations created.");
|
||||||
|
_workspaceCreatedCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.workspaces.created",
|
||||||
|
description: "Workspaces created.");
|
||||||
|
_contentItemCreatedCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.content_items.created",
|
||||||
|
description: "Content items created.");
|
||||||
|
_commentCreatedCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.comments.created",
|
||||||
|
description: "Comments created.");
|
||||||
|
_approvalDecisionCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.approval_decisions.submitted",
|
||||||
|
description: "Approval decisions submitted.");
|
||||||
|
_feedbackSubmittedCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.feedback.submitted",
|
||||||
|
description: "Feedback reports submitted.");
|
||||||
|
_workspaceInviteCreatedCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.workspace_invites.created",
|
||||||
|
description: "Workspace invites created.");
|
||||||
|
_emailDeliveryCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.email.delivery",
|
||||||
|
description: "Email delivery attempts partitioned by outcome and provider.");
|
||||||
|
_blobStorageOperationCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.blob_storage.operations",
|
||||||
|
description: "Blob storage operations partitioned by operation and outcome.");
|
||||||
|
_backgroundJobRunCounter = Meter.CreateCounter<long>(
|
||||||
|
"socialize.background_job.runs",
|
||||||
|
description: "Background job runs partitioned by job and outcome.");
|
||||||
|
|
||||||
|
Meter.CreateObservableGauge(
|
||||||
|
"socialize.workflow.content_items",
|
||||||
|
ObserveContentItemCounts,
|
||||||
|
description: "Current content item counts by status.");
|
||||||
|
Meter.CreateObservableGauge(
|
||||||
|
"socialize.workflow.feedback_reports",
|
||||||
|
ObserveFeedbackReportCounts,
|
||||||
|
description: "Current feedback report counts by status.");
|
||||||
|
Meter.CreateObservableGauge(
|
||||||
|
"socialize.workflow.pending_invites",
|
||||||
|
ObservePendingInviteCount,
|
||||||
|
description: "Current pending workspace invite count.");
|
||||||
|
Meter.CreateObservableGauge(
|
||||||
|
"socialize.workflow.stale_in_approval",
|
||||||
|
ObserveStaleApprovalCount,
|
||||||
|
description: "Current count of content items in approval longer than the configured stale threshold.");
|
||||||
|
Meter.CreateObservableGauge(
|
||||||
|
"socialize.workflow.active_workspaces",
|
||||||
|
ObserveActiveWorkspaceCounts,
|
||||||
|
description: "Current active workspace counts by observation window.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Meter Meter { get; }
|
||||||
|
|
||||||
|
public ActivitySource ActivitySource { get; }
|
||||||
|
|
||||||
|
public void RecordLoginAttempt(bool succeeded, string reason)
|
||||||
|
{
|
||||||
|
_loginAttemptCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("outcome", succeeded ? "success" : "failure"),
|
||||||
|
new KeyValuePair<string, object?>("reason", reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordOrganizationCreated(Guid organizationId)
|
||||||
|
{
|
||||||
|
_organizationCreatedCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("organization.id", organizationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordWorkspaceCreated(Guid organizationId, Guid workspaceId)
|
||||||
|
{
|
||||||
|
_workspaceCreatedCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("organization.id", organizationId),
|
||||||
|
new KeyValuePair<string, object?>("workspace.id", workspaceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordContentItemCreated(Guid workspaceId)
|
||||||
|
{
|
||||||
|
_contentItemCreatedCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("workspace.id", workspaceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordCommentCreated(Guid workspaceId, bool hasAttachment)
|
||||||
|
{
|
||||||
|
_commentCreatedCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("workspace.id", workspaceId),
|
||||||
|
new KeyValuePair<string, object?>("has_attachment", hasAttachment));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordApprovalDecisionSubmitted(Guid workspaceId, string decision)
|
||||||
|
{
|
||||||
|
_approvalDecisionCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("workspace.id", workspaceId),
|
||||||
|
new KeyValuePair<string, object?>("decision", decision));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordFeedbackSubmitted(string type, Guid? workspaceId)
|
||||||
|
{
|
||||||
|
_feedbackSubmittedCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("feedback.type", type),
|
||||||
|
new KeyValuePair<string, object?>("workspace.id", workspaceId?.ToString() ?? "none"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordWorkspaceInviteCreated(Guid workspaceId, string role)
|
||||||
|
{
|
||||||
|
_workspaceInviteCreatedCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("workspace.id", workspaceId),
|
||||||
|
new KeyValuePair<string, object?>("role", role));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordEmailDelivery(string provider, bool succeeded)
|
||||||
|
{
|
||||||
|
_emailDeliveryCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("provider", provider),
|
||||||
|
new KeyValuePair<string, object?>("outcome", succeeded ? "success" : "failure"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordBlobStorageOperation(string operation, bool succeeded)
|
||||||
|
{
|
||||||
|
_blobStorageOperationCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("operation", operation),
|
||||||
|
new KeyValuePair<string, object?>("outcome", succeeded ? "success" : "failure"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordBackgroundJobRun(string job, bool succeeded)
|
||||||
|
{
|
||||||
|
_backgroundJobRunCounter.Add(
|
||||||
|
1,
|
||||||
|
new KeyValuePair<string, object?>("job", job),
|
||||||
|
new KeyValuePair<string, object?>("outcome", succeeded ? "success" : "failure"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateWorkflowHealth(WorkflowHealthSnapshot snapshot)
|
||||||
|
{
|
||||||
|
lock (_workflowHealthLock)
|
||||||
|
{
|
||||||
|
_workflowHealthSnapshot = snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Meter.Dispose();
|
||||||
|
ActivitySource.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Measurement<int>[] ObserveContentItemCounts()
|
||||||
|
{
|
||||||
|
WorkflowHealthSnapshot snapshot = GetWorkflowHealthSnapshot();
|
||||||
|
return snapshot.ContentItemsByStatus
|
||||||
|
.Select(pair => new Measurement<int>(
|
||||||
|
pair.Value,
|
||||||
|
new KeyValuePair<string, object?>("status", pair.Key)))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Measurement<int>[] ObserveFeedbackReportCounts()
|
||||||
|
{
|
||||||
|
WorkflowHealthSnapshot snapshot = GetWorkflowHealthSnapshot();
|
||||||
|
return snapshot.FeedbackReportsByStatus
|
||||||
|
.Select(pair => new Measurement<int>(
|
||||||
|
pair.Value,
|
||||||
|
new KeyValuePair<string, object?>("status", pair.Key)))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Measurement<int> ObservePendingInviteCount()
|
||||||
|
{
|
||||||
|
return new Measurement<int>(GetWorkflowHealthSnapshot().PendingInviteCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Measurement<int> ObserveStaleApprovalCount()
|
||||||
|
{
|
||||||
|
return new Measurement<int>(GetWorkflowHealthSnapshot().StaleInApprovalCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Measurement<int>[] ObserveActiveWorkspaceCounts()
|
||||||
|
{
|
||||||
|
WorkflowHealthSnapshot snapshot = GetWorkflowHealthSnapshot();
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new Measurement<int>(
|
||||||
|
snapshot.ActiveWorkspaces24Hours,
|
||||||
|
new KeyValuePair<string, object?>("window", "24h")),
|
||||||
|
new Measurement<int>(
|
||||||
|
snapshot.ActiveWorkspaces7Days,
|
||||||
|
new KeyValuePair<string, object?>("window", "7d")),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private WorkflowHealthSnapshot GetWorkflowHealthSnapshot()
|
||||||
|
{
|
||||||
|
lock (_workflowHealthLock)
|
||||||
|
{
|
||||||
|
return _workflowHealthSnapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record WorkflowHealthSnapshot(
|
||||||
|
IReadOnlyDictionary<string, int> ContentItemsByStatus,
|
||||||
|
IReadOnlyDictionary<string, int> FeedbackReportsByStatus,
|
||||||
|
int PendingInviteCount,
|
||||||
|
int StaleInApprovalCount,
|
||||||
|
int ActiveWorkspaces24Hours,
|
||||||
|
int ActiveWorkspaces7Days)
|
||||||
|
{
|
||||||
|
public static WorkflowHealthSnapshot Empty { get; } = new(
|
||||||
|
new Dictionary<string, int>(StringComparer.Ordinal),
|
||||||
|
new Dictionary<string, int>(StringComparer.Ordinal),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Modules.Feedback.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Infrastructure.Observability;
|
||||||
|
|
||||||
|
internal sealed class WorkflowHealthSamplerService(
|
||||||
|
IServiceScopeFactory scopeFactory,
|
||||||
|
SocializeMetrics metrics,
|
||||||
|
ILogger<WorkflowHealthSamplerService> logger)
|
||||||
|
: BackgroundService
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan SampleInterval = TimeSpan.FromMinutes(5);
|
||||||
|
private static readonly TimeSpan StaleApprovalThreshold = TimeSpan.FromDays(3);
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
await SampleAsync(stoppingToken);
|
||||||
|
|
||||||
|
using PeriodicTimer timer = new(SampleInterval);
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await timer.WaitForNextTickAsync(stoppingToken);
|
||||||
|
await SampleAsync(stoppingToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
logger.LogDebug(ex, "Workflow health sampler stopped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SampleAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using IServiceScope scope = scopeFactory.CreateScope();
|
||||||
|
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
DateTimeOffset staleApprovalCutoff = now.Subtract(StaleApprovalThreshold);
|
||||||
|
DateTimeOffset active24HourCutoff = now.AddHours(-24);
|
||||||
|
DateTimeOffset active7DayCutoff = now.AddDays(-7);
|
||||||
|
|
||||||
|
Dictionary<string, int> contentItemsByStatus = await dbContext.ContentItems
|
||||||
|
.GroupBy(item => item.Status)
|
||||||
|
.Select(group => new { Status = group.Key, Count = group.Count() })
|
||||||
|
.ToDictionaryAsync(group => group.Status, group => group.Count, StringComparer.Ordinal, stoppingToken);
|
||||||
|
|
||||||
|
Dictionary<string, int> feedbackReportsByStatus = await dbContext.FeedbackReports
|
||||||
|
.GroupBy(report => report.Status)
|
||||||
|
.Select(group => new { Status = group.Key, Count = group.Count() })
|
||||||
|
.ToDictionaryAsync(
|
||||||
|
group => group.Status == FeedbackStatus.WontDo ? "WontDo" : group.Status.ToString(),
|
||||||
|
group => group.Count,
|
||||||
|
StringComparer.Ordinal,
|
||||||
|
stoppingToken);
|
||||||
|
|
||||||
|
int pendingInviteCount = await dbContext.WorkspaceInvites
|
||||||
|
.CountAsync(invite => invite.Status == WorkspaceInviteStatuses.Pending, stoppingToken);
|
||||||
|
|
||||||
|
int staleInApprovalCount = await dbContext.ContentItems
|
||||||
|
.CountAsync(
|
||||||
|
item => item.Status == "In approval" && item.CreatedAt <= staleApprovalCutoff,
|
||||||
|
stoppingToken);
|
||||||
|
|
||||||
|
int activeWorkspaces24Hours = await dbContext.ContentItemActivityEntries
|
||||||
|
.Where(entry => entry.CreatedAt >= active24HourCutoff)
|
||||||
|
.Select(entry => entry.WorkspaceId)
|
||||||
|
.Distinct()
|
||||||
|
.CountAsync(stoppingToken);
|
||||||
|
|
||||||
|
int activeWorkspaces7Days = await dbContext.ContentItemActivityEntries
|
||||||
|
.Where(entry => entry.CreatedAt >= active7DayCutoff)
|
||||||
|
.Select(entry => entry.WorkspaceId)
|
||||||
|
.Distinct()
|
||||||
|
.CountAsync(stoppingToken);
|
||||||
|
|
||||||
|
metrics.UpdateWorkflowHealth(new WorkflowHealthSnapshot(
|
||||||
|
contentItemsByStatus,
|
||||||
|
feedbackReportsByStatus,
|
||||||
|
pendingInviteCount,
|
||||||
|
staleInApprovalCount,
|
||||||
|
activeWorkspaces24Hours,
|
||||||
|
activeWorkspaces7Days));
|
||||||
|
metrics.RecordBackgroundJobRun(nameof(WorkflowHealthSamplerService), true);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException ex) when (stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
logger.LogDebug(ex, "Workflow health sampler stopped.");
|
||||||
|
}
|
||||||
|
#pragma warning disable CA1031
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
metrics.RecordBackgroundJobRun(nameof(WorkflowHealthSamplerService), false);
|
||||||
|
logger.LogError(ex, "Workflow health sampling failed.");
|
||||||
|
}
|
||||||
|
#pragma warning restore CA1031
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Payments.Stripe.Configuration;
|
namespace Socialize.Api.Infrastructure.Payments.Stripe.Configuration;
|
||||||
|
|
||||||
public class StripeOptions
|
internal class StripeOptions
|
||||||
{
|
{
|
||||||
public const string ConfigurationSection = "Stripe";
|
public const string ConfigurationSection = "Stripe";
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,175 @@
|
|||||||
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
|
internal sealed class AccessScopeService(
|
||||||
|
OrganizationAccessService organizationAccessService)
|
||||||
{
|
{
|
||||||
public bool IsManager(ClaimsPrincipal user)
|
public static bool IsManager(ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
return user.IsInRole(KnownRoles.Administrator) || user.IsInRole(KnownRoles.Manager);
|
return user.IsInRole(KnownRoles.Administrator) || user.IsInRole(KnownRoles.Manager);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsProvider(ClaimsPrincipal user)
|
public static bool IsProvider(ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
return user.IsInRole(KnownRoles.Provider);
|
return user.IsInRole(KnownRoles.Provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsClient(ClaimsPrincipal user)
|
public static bool IsClient(ClaimsPrincipal user)
|
||||||
{
|
{
|
||||||
return user.IsInRole(KnownRoles.Client);
|
return user.IsInRole(KnownRoles.Client);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanAccessWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
public static bool CanAccessWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
||||||
{
|
{
|
||||||
return IsManager(user) || user.GetWorkspaceScopeIds().Contains(workspaceId);
|
return user.GetWorkspaceScopeIds().Contains(workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanManageWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
public static bool CanManageWorkspace(ClaimsPrincipal user, Guid workspaceId)
|
||||||
{
|
{
|
||||||
return IsManager(user) && CanAccessWorkspace(user, workspaceId);
|
return IsManager(user) && CanAccessWorkspace(user, workspaceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanAccessClient(ClaimsPrincipal user, Guid workspaceId, Guid clientId)
|
public static bool CanAccessClient(ClaimsPrincipal user, Guid workspaceId, Guid clientId)
|
||||||
{
|
{
|
||||||
return IsManager(user)
|
return CanAccessWorkspace(user, workspaceId) &&
|
||||||
|| (CanAccessWorkspace(user, workspaceId) && user.GetClientScopeIds().Contains(clientId));
|
(IsManager(user) || user.GetClientScopeIds().Contains(clientId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanAccessProject(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
|
public static bool CanAccessCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||||
{
|
{
|
||||||
return IsManager(user)
|
return CanAccessClient(user, workspaceId, clientId) &&
|
||||||
|| (CanAccessClient(user, workspaceId, clientId) && user.GetProjectScopeIds().Contains(projectId));
|
(IsManager(user) || user.GetCampaignScopeIds().Contains(campaignId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanContributeToProject(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
|
public static bool CanContributeToCampaign(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||||
{
|
{
|
||||||
return IsManager(user) || (IsProvider(user) && CanAccessProject(user, workspaceId, clientId, projectId));
|
return IsManager(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|
||||||
|
|| IsProvider(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid projectId)
|
public static bool CanReviewContent(ClaimsPrincipal user, Guid workspaceId, Guid clientId, Guid campaignId)
|
||||||
{
|
{
|
||||||
return IsManager(user)
|
return IsManager(user) && CanAccessCampaign(user, workspaceId, clientId, campaignId)
|
||||||
|| IsProvider(user) && CanAccessProject(user, workspaceId, clientId, projectId)
|
|| 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 user.GetWorkspaceScopeIds().Contains(workspaceId)
|
||||||
|
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync(
|
||||||
|
user,
|
||||||
|
workspaceId,
|
||||||
|
OrganizationPermissions.AccessOwnedWorkspaces,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanManageWorkspaceAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
Guid workspaceId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
return CanManageWorkspace(user, workspaceId)
|
||||||
|
|| await organizationAccessService.HasInheritedWorkspacePermissionAsync(
|
||||||
|
user,
|
||||||
|
workspaceId,
|
||||||
|
OrganizationPermissions.ManageWorkspaces,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanCreateWorkspaceAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
Guid organizationId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await organizationAccessService.HasOrganizationPermissionAsync(
|
||||||
|
user,
|
||||||
|
organizationId,
|
||||||
|
OrganizationPermissions.CreateWorkspaces,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CanAccessClientAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
Guid workspaceId,
|
||||||
|
Guid clientId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (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 (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 CanAccessCampaignAsync(user, workspaceId, clientId, campaignId, ct)
|
||||||
|
|| 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 CanAccessCampaignAsync(user, workspaceId, clientId, campaignId, ct)
|
||||||
|
|| 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,8 +1,9 @@
|
|||||||
using System.Security.Claims;
|
using System.Globalization;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class ClaimsPrincipalExtensions
|
internal static class ClaimsPrincipalExtensions
|
||||||
{
|
{
|
||||||
public static IReadOnlyCollection<Guid> GetScopeIds(this ClaimsPrincipal claims, string key)
|
public static IReadOnlyCollection<Guid> GetScopeIds(this ClaimsPrincipal claims, string key)
|
||||||
{
|
{
|
||||||
@@ -23,9 +24,9 @@ public static class ClaimsPrincipalExtensions
|
|||||||
return claims.GetScopeIds(KnownClaims.ClientScope);
|
return claims.GetScopeIds(KnownClaims.ClientScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyCollection<Guid> GetProjectScopeIds(this ClaimsPrincipal claims)
|
public static IReadOnlyCollection<Guid> GetCampaignScopeIds(this ClaimsPrincipal claims)
|
||||||
{
|
{
|
||||||
return claims.GetScopeIds(KnownClaims.ProjectScope);
|
return claims.GetScopeIds(KnownClaims.CampaignScope);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string? GetPersona(this ClaimsPrincipal claims)
|
public static string? GetPersona(this ClaimsPrincipal claims)
|
||||||
@@ -81,11 +82,11 @@ public static class ClaimsPrincipalExtensions
|
|||||||
|
|
||||||
if (claim is null)
|
if (claim is null)
|
||||||
{
|
{
|
||||||
throw new MissingClaimException(key);
|
throw MissingClaimException.ForClaim(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return typeof(TValue) == typeof(Guid)
|
return typeof(TValue) == typeof(Guid)
|
||||||
? Guid.Parse(claim.Value)
|
? Guid.Parse(claim.Value)
|
||||||
: Convert.ChangeType(claim.Value, typeof(TValue));
|
: Convert.ChangeType(claim.Value, typeof(TValue), CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using Microsoft.IdentityModel.Tokens;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class JwtTokenHelper
|
internal static class JwtTokenHelper
|
||||||
{
|
{
|
||||||
public static string GenerateJwtToken(
|
public static string GenerateJwtToken(
|
||||||
TimeSpan expiresIn,
|
TimeSpan expiresIn,
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class KnownClaims
|
internal static class KnownClaims
|
||||||
{
|
{
|
||||||
public const string Alias = "alias";
|
public const string Alias = "alias";
|
||||||
public const string PortraitUrl = "portraitUrl";
|
public const string PortraitUrl = "portraitUrl";
|
||||||
public const string WorkspaceScope = "workspace";
|
public const string WorkspaceScope = "workspace";
|
||||||
public const string ClientScope = "client";
|
public const string ClientScope = "client";
|
||||||
public const string ProjectScope = "project";
|
public const string CampaignScope = "campaign";
|
||||||
public const string Persona = "persona";
|
public const string Persona = "persona";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public class MissingClaimException(
|
public class MissingClaimException : Exception
|
||||||
string claimName)
|
{
|
||||||
: Exception($"Claim '{claimName}' is missing.");
|
public MissingClaimException()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MissingClaimException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public MissingClaimException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static MissingClaimException ForClaim(string claimName)
|
||||||
|
{
|
||||||
|
return new MissingClaimException($"Claim '{claimName}' is missing.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ using System.Text;
|
|||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
// If we need to add special characters we can alternate between 2 pools.
|
// If we need to add special characters we can alternate between 2 pools.
|
||||||
public static class PasswordGenerator
|
internal static class PasswordGenerator
|
||||||
{
|
{
|
||||||
private const string LowerLetters = "abcdefghijklmnopqrstuvwxyz";
|
private const string LowerLetters = "abcdefghijklmnopqrstuvwxyz";
|
||||||
private const string UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
private const string UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||||
private const string Numbers = "0123456789";
|
private const string Numbers = "0123456789";
|
||||||
private const string SpecialCharacters = "!@#$%^&*()_+-=[];',./`~{}|:\"<>?";
|
private const string SpecialCharacters = "!@#$%^&*()_+-=[];',./`~{}|:\"<>?";
|
||||||
|
|
||||||
private static readonly Random Random = new();
|
|
||||||
|
|
||||||
public static string Next(
|
public static string Next(
|
||||||
int length = 15,
|
int length = 15,
|
||||||
bool requireNumber = true,
|
bool requireNumber = true,
|
||||||
@@ -23,7 +21,7 @@ public static class PasswordGenerator
|
|||||||
// Create pools based on the requirements
|
// Create pools based on the requirements
|
||||||
StringBuilder characterPool = new();
|
StringBuilder characterPool = new();
|
||||||
|
|
||||||
if (requireNumber)
|
if (requireLowercase)
|
||||||
{
|
{
|
||||||
characterPool.Append(LowerLetters);
|
characterPool.Append(LowerLetters);
|
||||||
}
|
}
|
||||||
@@ -51,22 +49,22 @@ public static class PasswordGenerator
|
|||||||
|
|
||||||
if (requireLowercase)
|
if (requireLowercase)
|
||||||
{
|
{
|
||||||
password[index++] = LowerLetters[Random.Next(LowerLetters.Length)];
|
password[index++] = LowerLetters[RandomNumberGenerator.GetInt32(LowerLetters.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireCapital)
|
if (requireCapital)
|
||||||
{
|
{
|
||||||
password[index++] = UpperLetters[Random.Next(UpperLetters.Length)];
|
password[index++] = UpperLetters[RandomNumberGenerator.GetInt32(UpperLetters.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireNumber)
|
if (requireNumber)
|
||||||
{
|
{
|
||||||
password[index++] = Numbers[Random.Next(Numbers.Length)];
|
password[index++] = Numbers[RandomNumberGenerator.GetInt32(Numbers.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireSpecialCharacter)
|
if (requireSpecialCharacter)
|
||||||
{
|
{
|
||||||
password[index++] = SpecialCharacters[Random.Next(SpecialCharacters.Length)];
|
password[index++] = SpecialCharacters[RandomNumberGenerator.GetInt32(SpecialCharacters.Length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fill the rest with the password
|
// Fill the rest with the password
|
||||||
@@ -85,7 +83,7 @@ public static class PasswordGenerator
|
|||||||
{
|
{
|
||||||
for (int i = array.Length - 1; i > 0; i--)
|
for (int i = array.Length - 1; i > 0; i--)
|
||||||
{
|
{
|
||||||
int j = Random.Next(i + 1);
|
int j = RandomNumberGenerator.GetInt32(i + 1);
|
||||||
(array[i], array[j]) = (array[j], array[i]); // Swap elements
|
(array[i], array[j]) = (array[j], array[i]); // Swap elements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using System.Security.Cryptography;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Security;
|
namespace Socialize.Api.Infrastructure.Security;
|
||||||
|
|
||||||
public static class RefreshTokenGenerator
|
internal static class RefreshTokenGenerator
|
||||||
{
|
{
|
||||||
public static string Next()
|
public static string Next()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,24 +6,33 @@ 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.Projects.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;
|
|
||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.Development;
|
namespace Socialize.Api.Infrastructure.TestData;
|
||||||
|
|
||||||
public static class DevelopmentSeedExtensions
|
#pragma warning disable S1075 // Test data intentionally uses representative external URLs.
|
||||||
|
|
||||||
|
internal static class TestDataSeedExtensions
|
||||||
{
|
{
|
||||||
|
private static readonly Guid OrganizationId = Guid.Parse("99999999-9999-9999-9999-999999999999");
|
||||||
private static readonly Guid 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 ScopedProjectId = Guid.Parse("33333333-3333-3333-3333-333333333333");
|
private static readonly Guid ScopedCampaignId = Guid.Parse("33333333-3333-3333-3333-333333333333");
|
||||||
private static readonly Guid HiddenProjectId = 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");
|
||||||
@@ -31,23 +40,11 @@ public static class DevelopmentSeedExtensions
|
|||||||
private static readonly Guid ClientCommentId = Guid.Parse("77777777-7777-7777-7777-777777777777");
|
private static readonly Guid ClientCommentId = Guid.Parse("77777777-7777-7777-7777-777777777777");
|
||||||
private static readonly Guid NotificationId = Guid.Parse("88888888-8888-8888-8888-888888888888");
|
private static readonly Guid NotificationId = Guid.Parse("88888888-8888-8888-8888-888888888888");
|
||||||
|
|
||||||
public static async Task<IApplicationBuilder> UseDevelopmentSeedAsync(
|
public static async Task<IServiceProvider> SeedTestDataAsync(
|
||||||
this IApplicationBuilder app,
|
this IServiceProvider services,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
IHostEnvironment environment = app.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
using IServiceScope scope = services.CreateScope();
|
||||||
if (!environment.IsDevelopment())
|
|
||||||
{
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
using IServiceScope scope = app.ApplicationServices.CreateScope();
|
|
||||||
IOptions<DevelopmentSeedOptions> options = scope.ServiceProvider.GetRequiredService<IOptions<DevelopmentSeedOptions>>();
|
|
||||||
if (!options.Value.Enabled)
|
|
||||||
{
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
|
|
||||||
UserManager userManager = scope.ServiceProvider.GetRequiredService<UserManager>();
|
UserManager userManager = scope.ServiceProvider.GetRequiredService<UserManager>();
|
||||||
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
AppDbContext dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
|
||||||
@@ -56,7 +53,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
id: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
id: Guid.Parse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
|
||||||
username: "manager",
|
username: "manager",
|
||||||
email: "manager@socialize.local",
|
email: "manager@socialize.local",
|
||||||
password: "manager",
|
password: "Manager1!",
|
||||||
alias: "Northstar Manager",
|
alias: "Northstar Manager",
|
||||||
firstname: "Morgan",
|
firstname: "Morgan",
|
||||||
lastname: "Reid",
|
lastname: "Reid",
|
||||||
@@ -72,7 +69,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
id: Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
id: Guid.Parse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"),
|
||||||
username: "client",
|
username: "client",
|
||||||
email: "client@socialize.local",
|
email: "client@socialize.local",
|
||||||
password: "client",
|
password: "Client1!",
|
||||||
alias: "Sofia Martin",
|
alias: "Sofia Martin",
|
||||||
firstname: "Sofia",
|
firstname: "Sofia",
|
||||||
lastname: "Martin",
|
lastname: "Martin",
|
||||||
@@ -89,7 +86,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
id: Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
|
id: Guid.Parse("cccccccc-cccc-cccc-cccc-cccccccccccc"),
|
||||||
username: "provider",
|
username: "provider",
|
||||||
email: "provider@socialize.local",
|
email: "provider@socialize.local",
|
||||||
password: "provider",
|
password: "Provider1!",
|
||||||
alias: "Alex Studio",
|
alias: "Alex Studio",
|
||||||
firstname: "Alex",
|
firstname: "Alex",
|
||||||
lastname: "Studio",
|
lastname: "Studio",
|
||||||
@@ -99,7 +96,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
[
|
[
|
||||||
new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()),
|
new Claim(KnownClaims.WorkspaceScope, WorkspaceId.ToString()),
|
||||||
new Claim(KnownClaims.ClientScope, ScopedClientId.ToString()),
|
new Claim(KnownClaims.ClientScope, ScopedClientId.ToString()),
|
||||||
new Claim(KnownClaims.ProjectScope, ScopedProjectId.ToString()),
|
new Claim(KnownClaims.CampaignScope, ScopedCampaignId.ToString()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
User dev = await EnsureUserAsync(
|
User dev = await EnsureUserAsync(
|
||||||
@@ -107,7 +104,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
id: Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"),
|
id: Guid.Parse("dddddddd-dddd-dddd-dddd-dddddddddddd"),
|
||||||
username: "dev",
|
username: "dev",
|
||||||
email: "dev@socialize.local",
|
email: "dev@socialize.local",
|
||||||
password: "dev",
|
password: "Developer1!",
|
||||||
alias: "Socialize Dev",
|
alias: "Socialize Dev",
|
||||||
firstname: "Jo",
|
firstname: "Jo",
|
||||||
lastname: "Bumble",
|
lastname: "Bumble",
|
||||||
@@ -117,6 +114,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,
|
||||||
@@ -124,7 +127,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
dbContext,
|
dbContext,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return app;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<User> EnsureUserAsync(
|
private static async Task<User> EnsureUserAsync(
|
||||||
@@ -161,7 +164,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
if (!createResult.Succeeded)
|
if (!createResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Failed to seed development user '{username}': {string.Join(", ", createResult.Errors.Select(error => error.Description))}");
|
$"Failed to seed test user '{username}': {string.Join(", ", createResult.Errors.Select(error => error.Description))}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +184,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
if (!passwordResetResult.Succeeded)
|
if (!passwordResetResult.Succeeded)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
$"Failed to set development password for '{username}': {string.Join(", ", passwordResetResult.Errors.Select(error => error.Description))}");
|
$"Failed to set test password for '{username}': {string.Join(", ", passwordResetResult.Errors.Select(error => error.Description))}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +203,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
|
|
||||||
IList<Claim> existingClaims = await userManager.GetClaimsAsync(user);
|
IList<Claim> existingClaims = await userManager.GetClaimsAsync(user);
|
||||||
List<Claim> managedClaims = existingClaims
|
List<Claim> managedClaims = existingClaims
|
||||||
.Where(claim => claim.Type is KnownClaims.WorkspaceScope or KnownClaims.ClientScope or KnownClaims.ProjectScope or KnownClaims.Persona)
|
.Where(claim => claim.Type is KnownClaims.WorkspaceScope or KnownClaims.ClientScope or KnownClaims.CampaignScope or KnownClaims.Persona)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (Claim claim in managedClaims)
|
foreach (Claim claim in managedClaims)
|
||||||
@@ -208,13 +211,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
await userManager.RemoveClaimAsync(user, claim);
|
await userManager.RemoveClaimAsync(user, claim);
|
||||||
}
|
}
|
||||||
|
|
||||||
string persona = roles.Contains(KnownRoles.Manager, StringComparer.Ordinal)
|
string persona = GetPersona(roles);
|
||||||
? KnownRoles.Manager
|
|
||||||
: roles.Contains(KnownRoles.Client, StringComparer.Ordinal)
|
|
||||||
? KnownRoles.Client
|
|
||||||
: roles.Contains(KnownRoles.Provider, StringComparer.Ordinal)
|
|
||||||
? KnownRoles.Provider
|
|
||||||
: KnownRoles.WorkspaceMember;
|
|
||||||
|
|
||||||
foreach (Claim claim in claims.Concat([new Claim(KnownClaims.Persona, persona)]))
|
foreach (Claim claim in claims.Concat([new Claim(KnownClaims.Persona, persona)]))
|
||||||
{
|
{
|
||||||
@@ -224,6 +221,96 @@ public static class DevelopmentSeedExtensions
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetPersona(IReadOnlyCollection<string> roles)
|
||||||
|
{
|
||||||
|
if (roles.Contains(KnownRoles.Manager, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return KnownRoles.Manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roles.Contains(KnownRoles.Client, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return KnownRoles.Client;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roles.Contains(KnownRoles.Provider, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
return KnownRoles.Provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
return KnownRoles.WorkspaceMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task EnsureOrganizationDataAsync(
|
||||||
|
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.MembershipTierId = OrganizationMembershipTierSeed.AgencyId;
|
||||||
|
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 +318,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,15 +352,15 @@ 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 UpsertProjectAsync(
|
await UpsertCampaignAsync(
|
||||||
dbContext,
|
dbContext,
|
||||||
ScopedProjectId,
|
ScopedCampaignId,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
ScopedClientId,
|
ScopedClientId,
|
||||||
"Spring Launch",
|
"Spring Launch",
|
||||||
@@ -285,10 +370,10 @@ public static class DevelopmentSeedExtensions
|
|||||||
"Cross-channel launch campaign for the spring offer.",
|
"Cross-channel launch campaign for the spring offer.",
|
||||||
"Coordinate creative approvals before the final week.",
|
"Coordinate creative approvals before the final week.",
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
await UpsertProjectAsync(
|
await UpsertCampaignAsync(
|
||||||
dbContext,
|
dbContext,
|
||||||
HiddenProjectId,
|
HiddenCampaignId,
|
||||||
WorkspaceId,
|
AtlasWorkspaceId,
|
||||||
HiddenClientId,
|
HiddenClientId,
|
||||||
"Summer Retention",
|
"Summer Retention",
|
||||||
"Planned",
|
"Planned",
|
||||||
@@ -298,16 +383,44 @@ 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,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
ScopedClientId,
|
ScopedClientId,
|
||||||
ScopedProjectId,
|
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 client review",
|
"In approval",
|
||||||
DateTimeOffset.UtcNow.AddDays(3),
|
DateTimeOffset.UtcNow.AddDays(3),
|
||||||
"v3",
|
"v3",
|
||||||
3,
|
3,
|
||||||
@@ -315,22 +428,22 @@ public static class DevelopmentSeedExtensions
|
|||||||
await UpsertContentItemAsync(
|
await UpsertContentItemAsync(
|
||||||
dbContext,
|
dbContext,
|
||||||
HiddenContentItemId,
|
HiddenContentItemId,
|
||||||
WorkspaceId,
|
AtlasWorkspaceId,
|
||||||
HiddenClientId,
|
HiddenClientId,
|
||||||
HiddenProjectId,
|
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 +491,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 +569,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,
|
||||||
@@ -491,7 +634,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task UpsertProjectAsync(
|
private static async Task UpsertCampaignAsync(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
Guid id,
|
Guid id,
|
||||||
Guid workspaceId,
|
Guid workspaceId,
|
||||||
@@ -504,26 +647,57 @@ public static class DevelopmentSeedExtensions
|
|||||||
string? notes,
|
string? notes,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Project? project = await dbContext.Projects.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
|
Campaign? campaign = await dbContext.Campaigns.SingleOrDefaultAsync(candidate => candidate.Id == id, cancellationToken);
|
||||||
if (project is null)
|
if (campaign is null)
|
||||||
{
|
{
|
||||||
project = new Project
|
campaign = new Campaign
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
Name = string.Empty,
|
Name = string.Empty,
|
||||||
Status = string.Empty,
|
Status = string.Empty,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
dbContext.Projects.Add(project);
|
dbContext.Campaigns.Add(campaign);
|
||||||
}
|
}
|
||||||
project.WorkspaceId = workspaceId;
|
campaign.WorkspaceId = workspaceId;
|
||||||
project.ClientId = clientId;
|
campaign.ClientId = clientId;
|
||||||
project.Name = name;
|
campaign.Name = name;
|
||||||
project.Description = description;
|
campaign.Description = description;
|
||||||
project.Notes = notes;
|
campaign.Notes = notes;
|
||||||
project.Status = status;
|
campaign.Status = status;
|
||||||
project.StartDate = startDate;
|
campaign.StartDate = startDate;
|
||||||
project.EndDate = endDate;
|
campaign.EndDate = endDate;
|
||||||
|
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);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,7 +706,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
Guid id,
|
Guid id,
|
||||||
Guid workspaceId,
|
Guid workspaceId,
|
||||||
Guid clientId,
|
Guid clientId,
|
||||||
Guid projectId,
|
Guid campaignId,
|
||||||
string title,
|
string title,
|
||||||
string publicationMessage,
|
string publicationMessage,
|
||||||
string publicationTargets,
|
string publicationTargets,
|
||||||
@@ -559,7 +733,7 @@ public static class DevelopmentSeedExtensions
|
|||||||
}
|
}
|
||||||
item.WorkspaceId = workspaceId;
|
item.WorkspaceId = workspaceId;
|
||||||
item.ClientId = clientId;
|
item.ClientId = clientId;
|
||||||
item.ProjectId = projectId;
|
item.CampaignId = campaignId;
|
||||||
item.Title = title;
|
item.Title = title;
|
||||||
item.PublicationMessage = publicationMessage;
|
item.PublicationMessage = publicationMessage;
|
||||||
item.PublicationTargets = publicationTargets;
|
item.PublicationTargets = publicationTargets;
|
||||||
@@ -2,7 +2,7 @@ using System.Text.RegularExpressions;
|
|||||||
|
|
||||||
namespace Socialize.Api.Infrastructure.YouTube;
|
namespace Socialize.Api.Infrastructure.YouTube;
|
||||||
|
|
||||||
public static class YouTubeUrlHelper
|
internal static class YouTubeUrlHelper
|
||||||
{
|
{
|
||||||
private static readonly Regex VideoIdRegex = new(
|
private static readonly Regex VideoIdRegex = new(
|
||||||
@"(?:youtube\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^""&?/\s]{11})",
|
@"(?:youtube\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|youtu\.be/)([^""&?/\s]{11})",
|
||||||
|
|||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,8 +12,8 @@ using Socialize.Api.Data;
|
|||||||
namespace Socialize.Api.Migrations
|
namespace Socialize.Api.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDbContext))]
|
[DbContext(typeof(AppDbContext))]
|
||||||
[Migration("20260430171959_AddFeedbackCommentsActivity")]
|
[Migration("20260507143849_Initial")]
|
||||||
partial class AddFeedbackCommentsActivity
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
@@ -219,6 +219,23 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(64)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(64)");
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("WorkflowInstanceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int?>("WorkflowStepRequiredApproverCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("WorkflowStepSortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("WorkflowStepTargetType")
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("WorkflowStepTargetValue")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
b.Property<Guid>("WorkspaceId")
|
b.Property<Guid>("WorkspaceId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -228,11 +245,103 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasIndex("ReviewerEmail");
|
b.HasIndex("ReviewerEmail");
|
||||||
|
|
||||||
|
b.HasIndex("WorkflowInstanceId");
|
||||||
|
|
||||||
b.HasIndex("WorkspaceId");
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
b.ToTable("ApprovalRequests", (string)null);
|
b.ToTable("ApprovalRequests", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.ApprovalWorkflowInstance", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ApprovalMode")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("CompletedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("ContentItemId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("StartedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("State")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)");
|
||||||
|
|
||||||
|
b.Property<Guid>("WorkspaceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ContentItemId");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
|
b.HasIndex("ContentItemId", "State")
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("\"State\" = 'Pending'");
|
||||||
|
|
||||||
|
b.ToTable("ApprovalWorkflowInstances", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Socialize.Api.Modules.Approvals.Data.WorkspaceApprovalStepConfiguration", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<int>("RequiredApproverCount")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(1);
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TargetType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)");
|
||||||
|
|
||||||
|
b.Property<string>("TargetValue")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<Guid>("WorkspaceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
|
b.HasIndex("WorkspaceId", "SortOrder")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("WorkspaceApprovalStepConfigurations", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Assets.Data.Asset", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@@ -332,6 +441,421 @@ 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 =>
|
||||||
|
{
|
||||||
|
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("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")
|
||||||
@@ -388,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)
|
||||||
@@ -414,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");
|
||||||
|
|
||||||
@@ -443,6 +984,9 @@ namespace Socialize.Api.Migrations
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("CampaignId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<Guid>("ClientId")
|
b.Property<Guid>("ClientId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
@@ -466,9 +1010,6 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(1024)
|
.HasMaxLength(1024)
|
||||||
.HasColumnType("character varying(1024)");
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
b.Property<Guid>("ProjectId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("PublicationMessage")
|
b.Property<string>("PublicationMessage")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(4000)
|
.HasMaxLength(4000)
|
||||||
@@ -494,15 +1035,71 @@ namespace Socialize.Api.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("ClientId");
|
b.HasIndex("CampaignId");
|
||||||
|
|
||||||
b.HasIndex("ProjectId");
|
b.HasIndex("ClientId");
|
||||||
|
|
||||||
b.HasIndex("WorkspaceId");
|
b.HasIndex("WorkspaceId");
|
||||||
|
|
||||||
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")
|
||||||
@@ -678,6 +1275,13 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(1024)
|
.HasMaxLength(1024)
|
||||||
.HasColumnType("character varying(1024)");
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CampaignId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("CampaignName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
b.Property<string>("CancellationReason")
|
b.Property<string>("CancellationReason")
|
||||||
.HasMaxLength(2000)
|
.HasMaxLength(2000)
|
||||||
.HasColumnType("character varying(2000)");
|
.HasColumnType("character varying(2000)");
|
||||||
@@ -715,13 +1319,6 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Property<DateTimeOffset>("LastActivityAt")
|
b.Property<DateTimeOffset>("LastActivityAt")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<Guid?>("ProjectId")
|
|
||||||
.HasColumnType("uuid");
|
|
||||||
|
|
||||||
b.Property<string>("ProjectName")
|
|
||||||
.HasMaxLength(256)
|
|
||||||
.HasColumnType("character varying(256)");
|
|
||||||
|
|
||||||
b.Property<string>("ReporterDisplayName")
|
b.Property<string>("ReporterDisplayName")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(256)
|
.HasMaxLength(256)
|
||||||
@@ -1044,60 +1641,7 @@ namespace Socialize.Api.Migrations
|
|||||||
b.ToTable("NotificationEvents", (string)null);
|
b.ToTable("NotificationEvents", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Socialize.Api.Modules.Projects.Data.Project", b =>
|
modelBuilder.Entity("Socialize.Api.Modules.Organizations.Data.Organization", 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.Api.Modules.Workspaces.Data.Workspace", b =>
|
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
@@ -1120,10 +1664,94 @@ namespace Socialize.Api.Migrations
|
|||||||
b.Property<Guid>("OwnerUserId")
|
b.Property<Guid>("OwnerUserId")
|
||||||
.HasColumnType("uuid");
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
b.Property<string>("Slug")
|
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()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(64)
|
||||||
.HasColumnType("character varying(128)");
|
.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 =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("ApprovalMode")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(32)
|
||||||
|
.HasColumnType("character varying(32)")
|
||||||
|
.HasDefaultValue("Required");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
|
||||||
|
b.Property<bool>("LockContentAfterApproval")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<string>("LogoUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("OwnerUserId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("SchedulePostsAutomaticallyOnApproval")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("SendAutomaticApprovalReminders")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<string>("TimeZone")
|
b.Property<string>("TimeZone")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -1132,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);
|
||||||
});
|
});
|
||||||
@@ -1232,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")
|
||||||
@@ -1276,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");
|
||||||
1284
backend/src/Socialize.Api/Migrations/20260507143849_Initial.cs
Normal file
1284
backend/src/Socialize.Api/Migrations/20260507143849_Initial.cs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,405 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddMissingDomainForeignKeys : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_CampaignId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "CampaignId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_ClientId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "ClientId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_FeedbackReports_ContentItemId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "ContentItemId");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalDecisions_ApprovalRequests_ApprovalRequestId",
|
||||||
|
table: "ApprovalDecisions",
|
||||||
|
column: "ApprovalRequestId",
|
||||||
|
principalTable: "ApprovalRequests",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_ApprovalWorkflowInstances_WorkflowInstance~",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
column: "WorkflowInstanceId",
|
||||||
|
principalTable: "ApprovalWorkflowInstances",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_ContentItems_ContentItemId",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_Workspaces_WorkspaceId",
|
||||||
|
table: "ApprovalRequests",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowInstances_ContentItems_ContentItemId",
|
||||||
|
table: "ApprovalWorkflowInstances",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowInstances_Workspaces_WorkspaceId",
|
||||||
|
table: "ApprovalWorkflowInstances",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_AssetRevisions_Assets_AssetId",
|
||||||
|
table: "AssetRevisions",
|
||||||
|
column: "AssetId",
|
||||||
|
principalTable: "Assets",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Assets_ContentItems_ContentItemId",
|
||||||
|
table: "Assets",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Assets_Workspaces_WorkspaceId",
|
||||||
|
table: "Assets",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Campaigns_Clients_ClientId",
|
||||||
|
table: "Campaigns",
|
||||||
|
column: "ClientId",
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Campaigns_Workspaces_WorkspaceId",
|
||||||
|
table: "Campaigns",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Channels_Workspaces_WorkspaceId",
|
||||||
|
table: "Channels",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Clients_Workspaces_WorkspaceId",
|
||||||
|
table: "Clients",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Comments_Comments_ParentCommentId",
|
||||||
|
table: "Comments",
|
||||||
|
column: "ParentCommentId",
|
||||||
|
principalTable: "Comments",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Comments_ContentItems_ContentItemId",
|
||||||
|
table: "Comments",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Comments_Workspaces_WorkspaceId",
|
||||||
|
table: "Comments",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItemActivityEntries_ContentItems_ContentItemId",
|
||||||
|
table: "ContentItemActivityEntries",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItemActivityEntries_Workspaces_WorkspaceId",
|
||||||
|
table: "ContentItemActivityEntries",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItemRevisions_ContentItems_ContentItemId",
|
||||||
|
table: "ContentItemRevisions",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItems_Campaigns_CampaignId",
|
||||||
|
table: "ContentItems",
|
||||||
|
column: "CampaignId",
|
||||||
|
principalTable: "Campaigns",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItems_Clients_ClientId",
|
||||||
|
table: "ContentItems",
|
||||||
|
column: "ClientId",
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_ContentItems_Workspaces_WorkspaceId",
|
||||||
|
table: "ContentItems",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Campaigns_CampaignId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "CampaignId",
|
||||||
|
principalTable: "Campaigns",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Clients_ClientId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "ClientId",
|
||||||
|
principalTable: "Clients",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_FeedbackReports_ContentItems_ContentItemId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Workspaces_WorkspaceId",
|
||||||
|
table: "FeedbackReports",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_NotificationEvents_ContentItems_ContentItemId",
|
||||||
|
table: "NotificationEvents",
|
||||||
|
column: "ContentItemId",
|
||||||
|
principalTable: "ContentItems",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_NotificationEvents_Workspaces_WorkspaceId",
|
||||||
|
table: "NotificationEvents",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_WorkspaceApprovalStepConfigurations_Workspaces_WorkspaceId",
|
||||||
|
table: "WorkspaceApprovalStepConfigurations",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_WorkspaceInvites_Workspaces_WorkspaceId",
|
||||||
|
table: "WorkspaceInvites",
|
||||||
|
column: "WorkspaceId",
|
||||||
|
principalTable: "Workspaces",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalDecisions_ApprovalRequests_ApprovalRequestId",
|
||||||
|
table: "ApprovalDecisions");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_ApprovalWorkflowInstances_WorkflowInstance~",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_ContentItems_ContentItemId",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalRequests_Workspaces_WorkspaceId",
|
||||||
|
table: "ApprovalRequests");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowInstances_ContentItems_ContentItemId",
|
||||||
|
table: "ApprovalWorkflowInstances");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowInstances_Workspaces_WorkspaceId",
|
||||||
|
table: "ApprovalWorkflowInstances");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_AssetRevisions_Assets_AssetId",
|
||||||
|
table: "AssetRevisions");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Assets_ContentItems_ContentItemId",
|
||||||
|
table: "Assets");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Assets_Workspaces_WorkspaceId",
|
||||||
|
table: "Assets");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Campaigns_Clients_ClientId",
|
||||||
|
table: "Campaigns");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Campaigns_Workspaces_WorkspaceId",
|
||||||
|
table: "Campaigns");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Channels_Workspaces_WorkspaceId",
|
||||||
|
table: "Channels");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Clients_Workspaces_WorkspaceId",
|
||||||
|
table: "Clients");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Comments_Comments_ParentCommentId",
|
||||||
|
table: "Comments");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Comments_ContentItems_ContentItemId",
|
||||||
|
table: "Comments");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Comments_Workspaces_WorkspaceId",
|
||||||
|
table: "Comments");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItemActivityEntries_ContentItems_ContentItemId",
|
||||||
|
table: "ContentItemActivityEntries");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItemActivityEntries_Workspaces_WorkspaceId",
|
||||||
|
table: "ContentItemActivityEntries");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItemRevisions_ContentItems_ContentItemId",
|
||||||
|
table: "ContentItemRevisions");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItems_Campaigns_CampaignId",
|
||||||
|
table: "ContentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItems_Clients_ClientId",
|
||||||
|
table: "ContentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_ContentItems_Workspaces_WorkspaceId",
|
||||||
|
table: "ContentItems");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Campaigns_CampaignId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Clients_ClientId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_FeedbackReports_ContentItems_ContentItemId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_FeedbackReports_Workspaces_WorkspaceId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_NotificationEvents_ContentItems_ContentItemId",
|
||||||
|
table: "NotificationEvents");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_NotificationEvents_Workspaces_WorkspaceId",
|
||||||
|
table: "NotificationEvents");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_WorkspaceApprovalStepConfigurations_Workspaces_WorkspaceId",
|
||||||
|
table: "WorkspaceApprovalStepConfigurations");
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_WorkspaceInvites_Workspaces_WorkspaceId",
|
||||||
|
table: "WorkspaceInvites");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_FeedbackReports_CampaignId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_FeedbackReports_ClientId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_FeedbackReports_ContentItemId",
|
||||||
|
table: "FeedbackReports");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2293
backend/src/Socialize.Api/Migrations/20260508001846_AddOrganizationMembershipTiers.Designer.cs
generated
Normal file
2293
backend/src/Socialize.Api/Migrations/20260508001846_AddOrganizationMembershipTiers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddOrganizationMembershipTiers : Migration
|
||||||
|
{
|
||||||
|
private static readonly string[] MembershipTierSeedColumns =
|
||||||
|
[
|
||||||
|
"Id",
|
||||||
|
"ActiveContentLimit",
|
||||||
|
"Description",
|
||||||
|
"ExternalReviewerLimit",
|
||||||
|
"IsCustom",
|
||||||
|
"Key",
|
||||||
|
"MemberLimit",
|
||||||
|
"MonthlyPriceCents",
|
||||||
|
"Name",
|
||||||
|
"SortOrder",
|
||||||
|
"WorkspaceLimit"
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "MembershipTierId",
|
||||||
|
table: "Organizations",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: new Guid("20000000-0000-0000-0000-000000000001"));
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationMembershipTiers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Key = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||||
|
MonthlyPriceCents = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
WorkspaceLimit = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
ActiveContentLimit = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
MemberLimit = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
ExternalReviewerLimit = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
IsCustom = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
SortOrder = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationMembershipTiers", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
columns: MembershipTierSeedColumns,
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("20000000-0000-0000-0000-000000000001"), 3, "For trying Socialize on one real approval workflow.", 1, false, "free", 2, 0, "Free", 10, 1 },
|
||||||
|
{ new Guid("20000000-0000-0000-0000-000000000002"), 25, "For solo operators managing recurring client reviews.", 10, false, "freelance", 5, 1900, "Freelance", 20, 3 },
|
||||||
|
{ new Guid("20000000-0000-0000-0000-000000000003"), 250, "For agencies that need repeatable client approval operations.", null, false, "agency", 25, 7900, "Agency", 30, 15 },
|
||||||
|
{ new Guid("20000000-0000-0000-0000-000000000004"), null, "For larger organizations with governance and access needs.", null, true, "enterprise", null, null, "Enterprise", 40, null }
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Organizations_MembershipTierId",
|
||||||
|
table: "Organizations",
|
||||||
|
column: "MembershipTierId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationMembershipTiers_Key",
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationMembershipTiers_SortOrder",
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
column: "SortOrder");
|
||||||
|
|
||||||
|
migrationBuilder.AddForeignKey(
|
||||||
|
name: "FK_Organizations_OrganizationMembershipTiers_MembershipTierId",
|
||||||
|
table: "Organizations",
|
||||||
|
column: "MembershipTierId",
|
||||||
|
principalTable: "OrganizationMembershipTiers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropForeignKey(
|
||||||
|
name: "FK_Organizations_OrganizationMembershipTiers_MembershipTierId",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationMembershipTiers");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Organizations_MembershipTierId",
|
||||||
|
table: "Organizations");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "MembershipTierId",
|
||||||
|
table: "Organizations");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2382
backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs
generated
Normal file
2382
backend/src/Socialize.Api/Migrations/20260508003746_LocalizeOrganizationMembershipTiers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class LocalizeOrganizationMembershipTiers : Migration
|
||||||
|
{
|
||||||
|
private static readonly string[] MembershipTierTranslationSeedColumns =
|
||||||
|
[
|
||||||
|
"Id",
|
||||||
|
"Culture",
|
||||||
|
"Description",
|
||||||
|
"MembershipTierId",
|
||||||
|
"Name"
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly string[] MembershipTierColumnsToRestore =
|
||||||
|
[
|
||||||
|
"Description",
|
||||||
|
"Name"
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Description",
|
||||||
|
table: "OrganizationMembershipTiers");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Name",
|
||||||
|
table: "OrganizationMembershipTiers");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationMembershipTierTranslations",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
MembershipTierId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Culture = table.Column<string>(type: "character varying(16)", maxLength: 16, nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationMembershipTierTranslations", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OrganizationMembershipTierTranslations_OrganizationMembersh~",
|
||||||
|
column: x => x.MembershipTierId,
|
||||||
|
principalTable: "OrganizationMembershipTiers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "OrganizationMembershipTierTranslations",
|
||||||
|
columns: MembershipTierTranslationSeedColumns,
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000001"), "en", "For trying Socialize on one real approval workflow.", new Guid("20000000-0000-0000-0000-000000000001"), "Free" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000002"), "fr", "Pour essayer Socialize sur un vrai workflow d'approbation.", new Guid("20000000-0000-0000-0000-000000000001"), "Free" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000003"), "en", "For solo operators managing recurring client reviews.", new Guid("20000000-0000-0000-0000-000000000002"), "Freelance" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000004"), "fr", "Pour les independants qui gerent des revisions client recurrentes.", new Guid("20000000-0000-0000-0000-000000000002"), "Freelance" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000005"), "en", "For agencies that need repeatable client approval operations.", new Guid("20000000-0000-0000-0000-000000000003"), "Agency" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000006"), "fr", "Pour les agences qui veulent des operations d'approbation client repetables.", new Guid("20000000-0000-0000-0000-000000000003"), "Agency" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000007"), "en", "For larger organizations with governance and access needs.", new Guid("20000000-0000-0000-0000-000000000004"), "Enterprise" },
|
||||||
|
{ new Guid("20000000-0000-0001-0000-000000000008"), "fr", "Pour les grandes organisations avec des besoins de gouvernance et d'acces.", new Guid("20000000-0000-0000-0000-000000000004"), "Enterprise" }
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationMembershipTierTranslations_MembershipTierId_Cul~",
|
||||||
|
table: "OrganizationMembershipTierTranslations",
|
||||||
|
columns: ["MembershipTierId", "Culture"],
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationMembershipTierTranslations");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Description",
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Name",
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
type: "character varying(128)",
|
||||||
|
maxLength: 128,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000001"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For trying Socialize on one real approval workflow.", "Free" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000002"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For solo operators managing recurring client reviews.", "Freelance" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000003"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For agencies that need repeatable client approval operations.", "Agency" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "OrganizationMembershipTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: new Guid("20000000-0000-0000-0000-000000000004"),
|
||||||
|
columns: MembershipTierColumnsToRestore,
|
||||||
|
values: new object[] { "For larger organizations with governance and access needs.", "Enterprise" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2628
backend/src/Socialize.Api/Migrations/20260508010206_AddReleaseCommunications.Designer.cs
generated
Normal file
2628
backend/src/Socialize.Api/Migrations/20260508010206_AddReleaseCommunications.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,197 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddReleaseCommunications : Migration
|
||||||
|
{
|
||||||
|
private static readonly string[] ReleaseUpdateReadReceiptUniqueIndexColumns =
|
||||||
|
[
|
||||||
|
"ReleaseUpdateId",
|
||||||
|
"UserId",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||||
|
name: "LastAuthenticatedAt",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ReleaseUpdateEmailDigestReceipts",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
SentAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
UpdateCount = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ReleaseUpdateEmailDigestReceipts", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ReleaseUpdates",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Title = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: false),
|
||||||
|
Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||||
|
Body = table.Column<string>(type: "character varying(8000)", maxLength: 8000, nullable: true),
|
||||||
|
Category = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
Importance = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
Audience = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
Status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
DeploymentLabel = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||||
|
BuildVersion = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||||
|
CommitRange = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
CreatedByUserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||||
|
PublishedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
ArchivedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
ManualEmailSentByUserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
ManualEmailSentAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
ManualEmailAudience = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||||
|
ManualEmailRecipientCount = table.Column<int>(type: "integer", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ReleaseUpdates", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ReleaseCommits",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Sha = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||||
|
ShortSha = table.Column<string>(type: "character varying(16)", maxLength: 16, nullable: false),
|
||||||
|
Subject = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||||
|
AuthorName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
AuthorEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
AuthoredAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
CommittedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||||
|
SourceBranch = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||||
|
DeploymentLabel = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: true),
|
||||||
|
ExternalUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||||
|
CommunicationStatus = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||||
|
ReleaseUpdateId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||||
|
ImportedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"),
|
||||||
|
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ReleaseCommits", x => x.Sha);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ReleaseCommits_ReleaseUpdates_ReleaseUpdateId",
|
||||||
|
column: x => x.ReleaseUpdateId,
|
||||||
|
principalTable: "ReleaseUpdates",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.SetNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ReleaseUpdateReadReceipts",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ReleaseUpdateId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
ReadAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ReleaseUpdateReadReceipts", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ReleaseUpdateReadReceipts_ReleaseUpdates_ReleaseUpdateId",
|
||||||
|
column: x => x.ReleaseUpdateId,
|
||||||
|
principalTable: "ReleaseUpdates",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseCommits_CommittedAt",
|
||||||
|
table: "ReleaseCommits",
|
||||||
|
column: "CommittedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseCommits_CommunicationStatus",
|
||||||
|
table: "ReleaseCommits",
|
||||||
|
column: "CommunicationStatus");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseCommits_ReleaseUpdateId",
|
||||||
|
table: "ReleaseCommits",
|
||||||
|
column: "ReleaseUpdateId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdateEmailDigestReceipts_SentAt",
|
||||||
|
table: "ReleaseUpdateEmailDigestReceipts",
|
||||||
|
column: "SentAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdateEmailDigestReceipts_UserId",
|
||||||
|
table: "ReleaseUpdateEmailDigestReceipts",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdateReadReceipts_ReleaseUpdateId_UserId",
|
||||||
|
table: "ReleaseUpdateReadReceipts",
|
||||||
|
columns: ReleaseUpdateReadReceiptUniqueIndexColumns,
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdateReadReceipts_UserId",
|
||||||
|
table: "ReleaseUpdateReadReceipts",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdates_Audience",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
column: "Audience");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdates_CreatedByUserId",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
column: "CreatedByUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdates_PublishedAt",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
column: "PublishedAt");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdates_Status",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
column: "Status");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ReleaseCommits");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ReleaseUpdateEmailDigestReceipts");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ReleaseUpdateReadReceipts");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "LastAuthenticatedAt",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2591
backend/src/Socialize.Api/Migrations/20260508030349_SimplifyReleaseUpdates.Designer.cs
generated
Normal file
2591
backend/src/Socialize.Api/Migrations/20260508030349_SimplifyReleaseUpdates.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class SimplifyReleaseUpdates : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_ReleaseUpdates_Audience",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Audience",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Body",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "BuildVersion",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Category",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CommitRange",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "DeploymentLabel",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Importance",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ManualEmailAudience",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Audience",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Body",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(8000)",
|
||||||
|
maxLength: 8000,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "BuildVersion",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(128)",
|
||||||
|
maxLength: 128,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Category",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "CommitRange",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(256)",
|
||||||
|
maxLength: 256,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "DeploymentLabel",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(128)",
|
||||||
|
maxLength: 128,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Importance",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(32)",
|
||||||
|
maxLength: 32,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "ManualEmailAudience",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(64)",
|
||||||
|
maxLength: 64,
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ReleaseUpdates_Audience",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
column: "Audience");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2601
backend/src/Socialize.Api/Migrations/20260508031114_AddFrenchReleaseUpdateFields.Designer.cs
generated
Normal file
2601
backend/src/Socialize.Api/Migrations/20260508031114_AddFrenchReleaseUpdateFields.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddFrenchReleaseUpdateFields : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "SummaryFr",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "TitleFr",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(160)",
|
||||||
|
maxLength: 160,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
UPDATE "ReleaseUpdates"
|
||||||
|
SET "TitleFr" = "Title",
|
||||||
|
"SummaryFr" = "Summary"
|
||||||
|
WHERE "TitleFr" = '' AND "SummaryFr" = '';
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "SummaryFr",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "TitleFr",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2592
backend/src/Socialize.Api/Migrations/20260508034156_RemoveManualReleaseUpdateEmail.Designer.cs
generated
Normal file
2592
backend/src/Socialize.Api/Migrations/20260508034156_RemoveManualReleaseUpdateEmail.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class RemoveManualReleaseUpdateEmail : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ManualEmailRecipientCount",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ManualEmailSentAt",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "ManualEmailSentByUserId",
|
||||||
|
table: "ReleaseUpdates");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "ManualEmailRecipientCount",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "integer",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||||
|
name: "ManualEmailSentAt",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<Guid>(
|
||||||
|
name: "ManualEmailSentByUserId",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "uuid",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2592
backend/src/Socialize.Api/Migrations/20260508034902_ExpandReleaseUpdateDescriptions.Designer.cs
generated
Normal file
2592
backend/src/Socialize.Api/Migrations/20260508034902_ExpandReleaseUpdateDescriptions.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class ExpandReleaseUpdateDescriptions : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "SummaryFr",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(4000)",
|
||||||
|
maxLength: 4000,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(512)",
|
||||||
|
oldMaxLength: 512);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Summary",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(4000)",
|
||||||
|
maxLength: 4000,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(512)",
|
||||||
|
oldMaxLength: 512);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "SummaryFr",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(4000)",
|
||||||
|
oldMaxLength: 4000);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Summary",
|
||||||
|
table: "ReleaseUpdates",
|
||||||
|
type: "character varying(512)",
|
||||||
|
maxLength: 512,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(4000)",
|
||||||
|
oldMaxLength: 4000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2597
backend/src/Socialize.Api/Migrations/20260508122220_AddUserPreferredLanguage.Designer.cs
generated
Normal file
2597
backend/src/Socialize.Api/Migrations/20260508122220_AddUserPreferredLanguage.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddUserPreferredLanguage : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PreferredLanguage",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "character varying(8)",
|
||||||
|
maxLength: 8,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PreferredLanguage",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Approvals.Data;
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public class ApprovalDecision
|
internal class ApprovalDecision
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid ApprovalRequestId { get; set; }
|
public Guid ApprovalRequestId { get; set; }
|
||||||
|
|||||||
@@ -1,15 +1,43 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Data;
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public static class ApprovalModelConfiguration
|
internal static class ApprovalModelConfiguration
|
||||||
{
|
{
|
||||||
public static ModelBuilder ConfigureApprovalsModule(this ModelBuilder modelBuilder)
|
public static ModelBuilder ConfigureApprovalsModule(this ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
modelBuilder.Entity<ApprovalWorkflowInstance>(workflowInstance =>
|
||||||
|
{
|
||||||
|
workflowInstance.ToTable("ApprovalWorkflowInstances");
|
||||||
|
workflowInstance.HasKey(x => x.Id);
|
||||||
|
workflowInstance.Property(x => x.State).HasMaxLength(64).IsRequired();
|
||||||
|
workflowInstance.Property(x => x.ApprovalMode).HasMaxLength(64).IsRequired();
|
||||||
|
workflowInstance.Property(x => x.StartedAt)
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
workflowInstance.HasIndex(x => x.WorkspaceId);
|
||||||
|
workflowInstance.HasIndex(x => x.ContentItemId);
|
||||||
|
workflowInstance.HasIndex(x => new { x.ContentItemId, x.State })
|
||||||
|
.IsUnique()
|
||||||
|
.HasFilter("\"State\" = 'Pending'");
|
||||||
|
workflowInstance.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
workflowInstance.HasOne<ContentItem>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ContentItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
|
modelBuilder.Entity<ApprovalRequest>(approvalRequest =>
|
||||||
{
|
{
|
||||||
approvalRequest.ToTable("ApprovalRequests");
|
approvalRequest.ToTable("ApprovalRequests");
|
||||||
approvalRequest.HasKey(x => x.Id);
|
approvalRequest.HasKey(x => x.Id);
|
||||||
|
approvalRequest.Property(x => x.WorkflowStepTargetType).HasMaxLength(32);
|
||||||
|
approvalRequest.Property(x => x.WorkflowStepTargetValue).HasMaxLength(128);
|
||||||
approvalRequest.Property(x => x.Stage).HasMaxLength(64).IsRequired();
|
approvalRequest.Property(x => x.Stage).HasMaxLength(64).IsRequired();
|
||||||
approvalRequest.Property(x => x.ReviewerName).HasMaxLength(256).IsRequired();
|
approvalRequest.Property(x => x.ReviewerName).HasMaxLength(256).IsRequired();
|
||||||
approvalRequest.Property(x => x.ReviewerEmail).HasMaxLength(256).IsRequired();
|
approvalRequest.Property(x => x.ReviewerEmail).HasMaxLength(256).IsRequired();
|
||||||
@@ -20,7 +48,20 @@ public static class ApprovalModelConfiguration
|
|||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
approvalRequest.HasIndex(x => x.WorkspaceId);
|
approvalRequest.HasIndex(x => x.WorkspaceId);
|
||||||
approvalRequest.HasIndex(x => x.ContentItemId);
|
approvalRequest.HasIndex(x => x.ContentItemId);
|
||||||
|
approvalRequest.HasIndex(x => x.WorkflowInstanceId);
|
||||||
approvalRequest.HasIndex(x => x.ReviewerEmail);
|
approvalRequest.HasIndex(x => x.ReviewerEmail);
|
||||||
|
approvalRequest.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
approvalRequest.HasOne<ContentItem>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ContentItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
approvalRequest.HasOne<ApprovalWorkflowInstance>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkflowInstanceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<ApprovalDecision>(approvalDecision =>
|
modelBuilder.Entity<ApprovalDecision>(approvalDecision =>
|
||||||
@@ -35,6 +76,29 @@ public static class ApprovalModelConfiguration
|
|||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
approvalDecision.HasIndex(x => x.ApprovalRequestId);
|
approvalDecision.HasIndex(x => x.ApprovalRequestId);
|
||||||
|
approvalDecision.HasOne<ApprovalRequest>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ApprovalRequestId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<WorkspaceApprovalStepConfiguration>(approvalStep =>
|
||||||
|
{
|
||||||
|
approvalStep.ToTable("WorkspaceApprovalStepConfigurations");
|
||||||
|
approvalStep.HasKey(x => x.Id);
|
||||||
|
approvalStep.Property(x => x.Name).HasMaxLength(128).IsRequired();
|
||||||
|
approvalStep.Property(x => x.TargetType).HasMaxLength(32).IsRequired();
|
||||||
|
approvalStep.Property(x => x.TargetValue).HasMaxLength(128).IsRequired();
|
||||||
|
approvalStep.Property(x => x.RequiredApproverCount).HasDefaultValue(1);
|
||||||
|
approvalStep.Property(x => x.CreatedAt)
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
|
approvalStep.HasIndex(x => x.WorkspaceId);
|
||||||
|
approvalStep.HasIndex(x => new { x.WorkspaceId, x.SortOrder }).IsUnique();
|
||||||
|
approvalStep.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
return modelBuilder;
|
return modelBuilder;
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
namespace Socialize.Api.Modules.Approvals.Data;
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
public class ApprovalRequest
|
internal class ApprovalRequest
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
public Guid ContentItemId { get; set; }
|
public Guid ContentItemId { get; set; }
|
||||||
|
public Guid? WorkflowInstanceId { get; set; }
|
||||||
|
public int? WorkflowStepSortOrder { get; set; }
|
||||||
|
public string? WorkflowStepTargetType { get; set; }
|
||||||
|
public string? WorkflowStepTargetValue { get; set; }
|
||||||
|
public int? WorkflowStepRequiredApproverCount { get; set; }
|
||||||
public required string Stage { get; set; }
|
public required string Stage { get; set; }
|
||||||
public required string ReviewerName { get; set; }
|
public required string ReviewerName { get; set; }
|
||||||
public required string ReviewerEmail { get; set; }
|
public required string ReviewerEmail { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
|
internal class ApprovalWorkflowInstance
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public Guid WorkspaceId { get; set; }
|
||||||
|
public Guid ContentItemId { get; set; }
|
||||||
|
public required string State { get; set; }
|
||||||
|
public required string ApprovalMode { get; set; }
|
||||||
|
public DateTimeOffset StartedAt { get; init; }
|
||||||
|
public DateTimeOffset? CompletedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Socialize.Api.Modules.Approvals.Data;
|
||||||
|
|
||||||
|
internal class WorkspaceApprovalStepConfiguration
|
||||||
|
{
|
||||||
|
public Guid Id { get; init; }
|
||||||
|
public Guid WorkspaceId { get; set; }
|
||||||
|
public required string Name { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public required string TargetType { get; set; }
|
||||||
|
public required string TargetValue { get; set; }
|
||||||
|
public int RequiredApproverCount { get; set; } = 1;
|
||||||
|
public DateTimeOffset CreatedAt { get; init; }
|
||||||
|
}
|
||||||
@@ -1,123 +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.Notifications.Contracts;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (approval.Stage == "Internal")
|
|
||||||
{
|
|
||||||
contentItem.Status = "In internal review";
|
|
||||||
}
|
|
||||||
else if (approval.Stage == "Client")
|
|
||||||
{
|
|
||||||
contentItem.Status = "In client review";
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Stage,
|
|
||||||
approval.ReviewerName,
|
|
||||||
approval.ReviewerEmail,
|
|
||||||
approval.RequestedByUserId,
|
|
||||||
approval.DueAt,
|
|
||||||
approval.State,
|
|
||||||
approval.AccessToken,
|
|
||||||
approval.SentAt,
|
|
||||||
approval.CompletedAt,
|
|
||||||
[]);
|
|
||||||
|
|
||||||
await SendAsync(dto, StatusCodes.Status201Created, ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,9 +7,9 @@ using Socialize.Api.Infrastructure.Security;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||||
|
|
||||||
public record GetApprovalsRequest(Guid ContentItemId);
|
internal record GetApprovalsRequest(Guid ContentItemId);
|
||||||
|
|
||||||
public record ApprovalDecisionDto(
|
internal record ApprovalDecisionDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid ApprovalRequestId,
|
Guid ApprovalRequestId,
|
||||||
string Decision,
|
string Decision,
|
||||||
@@ -20,10 +20,15 @@ public record ApprovalDecisionDto(
|
|||||||
string? DecidedByPortraitUrl,
|
string? DecidedByPortraitUrl,
|
||||||
DateTimeOffset CreatedAt);
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
public record ApprovalRequestDto(
|
internal record ApprovalRequestDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ContentItemId,
|
Guid ContentItemId,
|
||||||
|
Guid? WorkflowInstanceId,
|
||||||
|
int? WorkflowStepSortOrder,
|
||||||
|
string? WorkflowStepTargetType,
|
||||||
|
string? WorkflowStepTargetValue,
|
||||||
|
int? WorkflowStepRequiredApproverCount,
|
||||||
string Stage,
|
string Stage,
|
||||||
string ReviewerName,
|
string ReviewerName,
|
||||||
string ReviewerEmail,
|
string ReviewerEmail,
|
||||||
@@ -35,7 +40,7 @@ public record ApprovalRequestDto(
|
|||||||
DateTimeOffset? CompletedAt,
|
DateTimeOffset? CompletedAt,
|
||||||
IReadOnlyCollection<ApprovalDecisionDto> Decisions);
|
IReadOnlyCollection<ApprovalDecisionDto> Decisions);
|
||||||
|
|
||||||
public class GetApprovalsHandler(
|
internal class GetApprovalsHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<GetApprovalsRequest, IReadOnlyCollection<ApprovalRequestDto>>
|
: Endpoint<GetApprovalsRequest, IReadOnlyCollection<ApprovalRequestDto>>
|
||||||
@@ -56,7 +61,7 @@ public class GetApprovalsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
|
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
@@ -65,6 +70,7 @@ public class GetApprovalsHandler(
|
|||||||
List<ApprovalRequest> approvals = await dbContext.ApprovalRequests
|
List<ApprovalRequest> approvals = await dbContext.ApprovalRequests
|
||||||
.Where(approval => approval.ContentItemId == request.ContentItemId)
|
.Where(approval => approval.ContentItemId == request.ContentItemId)
|
||||||
.OrderByDescending(approval => approval.SentAt)
|
.OrderByDescending(approval => approval.SentAt)
|
||||||
|
.ThenBy(approval => approval.WorkflowStepSortOrder)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
List<Guid> approvalIds = approvals
|
List<Guid> approvalIds = approvals
|
||||||
@@ -91,6 +97,11 @@ public class GetApprovalsHandler(
|
|||||||
approval.Id,
|
approval.Id,
|
||||||
approval.WorkspaceId,
|
approval.WorkspaceId,
|
||||||
approval.ContentItemId,
|
approval.ContentItemId,
|
||||||
|
approval.WorkflowInstanceId,
|
||||||
|
approval.WorkflowStepSortOrder,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue,
|
||||||
|
approval.WorkflowStepRequiredApproverCount,
|
||||||
approval.Stage,
|
approval.Stage,
|
||||||
approval.ReviewerName,
|
approval.ReviewerName,
|
||||||
approval.ReviewerEmail,
|
approval.ReviewerEmail,
|
||||||
|
|||||||
@@ -1,35 +1,45 @@
|
|||||||
using FastEndpoints;
|
using FastEndpoints;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Socialize.Api.Data;
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Observability;
|
||||||
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.Notifications.Contracts;
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals.Handlers;
|
namespace Socialize.Api.Modules.Approvals.Handlers;
|
||||||
|
|
||||||
public record SubmitApprovalDecisionRequest(
|
internal record SubmitApprovalDecisionRequest(
|
||||||
string Decision,
|
string Decision,
|
||||||
string? Comment,
|
|
||||||
string? ReviewerName,
|
string? ReviewerName,
|
||||||
string? ReviewerEmail);
|
string? ReviewerEmail);
|
||||||
|
|
||||||
public class SubmitApprovalDecisionRequestValidator
|
internal class SubmitApprovalDecisionRequestValidator
|
||||||
: Validator<SubmitApprovalDecisionRequest>
|
: Validator<SubmitApprovalDecisionRequest>
|
||||||
{
|
{
|
||||||
public SubmitApprovalDecisionRequestValidator()
|
public SubmitApprovalDecisionRequestValidator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.Decision).NotEmpty().MaximumLength(64);
|
RuleFor(x => x.Decision)
|
||||||
RuleFor(x => x.Comment).MaximumLength(2048);
|
.NotEmpty()
|
||||||
|
.Equal("Approved")
|
||||||
|
.WithMessage("Only approved decisions are supported.");
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SubmitApprovalDecisionHandler(
|
internal class SubmitApprovalDecisionHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService,
|
AccessScopeService accessScopeService,
|
||||||
INotificationEventWriter notificationEventWriter)
|
ApprovalWorkflowRuntimeService approvalWorkflowRuntimeService,
|
||||||
|
IContentItemActivityWriter activityWriter,
|
||||||
|
INotificationEventWriter notificationEventWriter,
|
||||||
|
SocializeMetrics metrics)
|
||||||
: Endpoint<SubmitApprovalDecisionRequest, ApprovalRequestDto>
|
: Endpoint<SubmitApprovalDecisionRequest, ApprovalRequestDto>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -58,71 +68,98 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (User?.Identity?.IsAuthenticated == true &&
|
if (User?.Identity?.IsAuthenticated == true &&
|
||||||
!accessScopeService.CanReviewContent(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
|
!await accessScopeService.CanReviewContentAsync(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Workspace? workspace = await dbContext.Workspaces.SingleOrDefaultAsync(candidate => candidate.Id == contentItem.WorkspaceId, ct);
|
||||||
|
if (workspace is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
string normalizedDecision = request.Decision.Trim();
|
string normalizedDecision = request.Decision.Trim();
|
||||||
string decidedByName = User?.Identity?.IsAuthenticated == true
|
ClaimsPrincipal? currentUser = User;
|
||||||
? User.GetAlias() ?? User.GetName()
|
bool isAuthenticated = currentUser?.Identity?.IsAuthenticated == true;
|
||||||
: string.IsNullOrWhiteSpace(request.ReviewerName) ? approval.ReviewerName : request.ReviewerName.Trim();
|
string decidedByName = isAuthenticated
|
||||||
string decidedByEmail = User?.Identity?.IsAuthenticated == true
|
? currentUser!.GetAlias() ?? currentUser!.GetName()
|
||||||
? User.GetEmail()
|
: GetReviewerName(request.ReviewerName, approval.ReviewerName);
|
||||||
: string.IsNullOrWhiteSpace(request.ReviewerEmail) ? approval.ReviewerEmail : request.ReviewerEmail.Trim();
|
string decidedByEmail = isAuthenticated
|
||||||
|
? currentUser!.GetEmail()
|
||||||
|
: GetReviewerEmail(request.ReviewerEmail, approval.ReviewerEmail);
|
||||||
|
|
||||||
ApprovalDecision decision = new()
|
ApprovalDecision decision = new()
|
||||||
{
|
{
|
||||||
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,
|
||||||
CreatedAt = DateTimeOffset.UtcNow,
|
CreatedAt = DateTimeOffset.UtcNow,
|
||||||
};
|
};
|
||||||
|
|
||||||
approval.State = normalizedDecision;
|
ApprovalWorkflowDecisionResult workflowDecisionResult = await approvalWorkflowRuntimeService
|
||||||
approval.CompletedAt = DateTimeOffset.UtcNow;
|
.ApplyWorkflowStepDecisionAsync(approval, contentItem, workspace, User!, decision, ct);
|
||||||
|
|
||||||
if (approval.Stage == "Internal")
|
if (!workflowDecisionResult.Succeeded)
|
||||||
{
|
{
|
||||||
contentItem.Status = normalizedDecision switch
|
AddError(request => request.Decision, workflowDecisionResult.ErrorMessage ?? "The approval decision could not be recorded.");
|
||||||
{
|
await SendErrorsAsync(workflowDecisionResult.StatusCode, ct);
|
||||||
"Approved" => "Ready for client review",
|
return;
|
||||||
"Changes requested" => "Changes requested internally",
|
|
||||||
"Rejected" => "Rejected",
|
|
||||||
_ => contentItem.Status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (approval.Stage == "Client")
|
|
||||||
{
|
|
||||||
contentItem.Status = normalizedDecision switch
|
|
||||||
{
|
|
||||||
"Approved" => "Approved",
|
|
||||||
"Changes requested" => "Changes requested by client",
|
|
||||||
"Rejected" => "Rejected",
|
|
||||||
_ => contentItem.Status,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dbContext.ApprovalDecisions.Add(decision);
|
if (!workflowDecisionResult.IsWorkflowStep)
|
||||||
await dbContext.SaveChangesAsync(ct);
|
{
|
||||||
|
approval.State = normalizedDecision;
|
||||||
|
approval.CompletedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
await notificationEventWriter.WriteAsync(
|
if (normalizedDecision == "Approved")
|
||||||
new NotificationEventWriteModel(
|
{
|
||||||
approval.WorkspaceId,
|
contentItem.Status = ApprovalWorkflowRules.GetFinalApprovalStatus(
|
||||||
approval.ContentItemId,
|
workspace.SchedulePostsAutomaticallyOnApproval,
|
||||||
"approval.decision.recorded",
|
contentItem.DueDate);
|
||||||
"ApprovalDecision",
|
}
|
||||||
decision.Id,
|
|
||||||
$"{decidedByName} recorded {normalizedDecision} for {contentItem.Title}.",
|
dbContext.ApprovalDecisions.Add(decision);
|
||||||
null,
|
await dbContext.SaveChangesAsync(ct);
|
||||||
decidedByEmail,
|
|
||||||
$$"""{"stage":"{{approval.Stage}}","status":"{{contentItem.Status}}"}"""),
|
await activityWriter.WriteAsync(
|
||||||
ct);
|
new ContentItemActivityWriteModel(
|
||||||
|
approval.WorkspaceId,
|
||||||
|
approval.ContentItemId,
|
||||||
|
"approval.decision.recorded",
|
||||||
|
"ApprovalDecision",
|
||||||
|
decision.Id,
|
||||||
|
$"{decidedByName} recorded {normalizedDecision} for {contentItem.Title}.",
|
||||||
|
decision.DecidedByUserId,
|
||||||
|
decidedByEmail,
|
||||||
|
JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
stage = approval.Stage,
|
||||||
|
status = contentItem.Status,
|
||||||
|
decision = normalizedDecision,
|
||||||
|
})),
|
||||||
|
ct);
|
||||||
|
|
||||||
|
await notificationEventWriter.WriteAsync(
|
||||||
|
new NotificationEventWriteModel(
|
||||||
|
approval.WorkspaceId,
|
||||||
|
approval.ContentItemId,
|
||||||
|
"approval.decision.recorded",
|
||||||
|
"ApprovalDecision",
|
||||||
|
decision.Id,
|
||||||
|
$"{decidedByName} recorded {normalizedDecision} for {contentItem.Title}.",
|
||||||
|
null,
|
||||||
|
decidedByEmail,
|
||||||
|
$$"""{"stage":"{{approval.Stage}}","status":"{{contentItem.Status}}"}"""),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
metrics.RecordApprovalDecisionSubmitted(approval.WorkspaceId, normalizedDecision);
|
||||||
|
|
||||||
List<ApprovalDecision> decisions = await dbContext.ApprovalDecisions
|
List<ApprovalDecision> decisions = await dbContext.ApprovalDecisions
|
||||||
.Where(candidate => candidate.ApprovalRequestId == approval.Id)
|
.Where(candidate => candidate.ApprovalRequestId == approval.Id)
|
||||||
@@ -158,6 +195,11 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
approval.Id,
|
approval.Id,
|
||||||
approval.WorkspaceId,
|
approval.WorkspaceId,
|
||||||
approval.ContentItemId,
|
approval.ContentItemId,
|
||||||
|
approval.WorkflowInstanceId,
|
||||||
|
approval.WorkflowStepSortOrder,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue,
|
||||||
|
approval.WorkflowStepRequiredApproverCount,
|
||||||
approval.Stage,
|
approval.Stage,
|
||||||
approval.ReviewerName,
|
approval.ReviewerName,
|
||||||
approval.ReviewerEmail,
|
approval.ReviewerEmail,
|
||||||
@@ -171,4 +213,18 @@ public class SubmitApprovalDecisionHandler(
|
|||||||
|
|
||||||
await SendOkAsync(dto, ct);
|
await SendOkAsync(dto, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetReviewerName(string? requestedName, string fallbackName)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(requestedName)
|
||||||
|
? fallbackName
|
||||||
|
: requestedName.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetReviewerEmail(string? requestedEmail, string fallbackEmail)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(requestedEmail)
|
||||||
|
? fallbackEmail
|
||||||
|
: requestedEmail.Trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
using Socialize.Api.Modules.Approvals.Data;
|
using Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Approvals;
|
namespace Socialize.Api.Modules.Approvals;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class ModuleRegistration
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder AddApprovalsModule(
|
public static WebApplicationBuilder AddApprovalsModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
{
|
{
|
||||||
|
builder.Services.AddScoped<ApprovalWorkflowRuntimeService>();
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
|
internal static class ApprovalStepTargetTypes
|
||||||
|
{
|
||||||
|
public const string Role = "Role";
|
||||||
|
public const string Membership = "Membership";
|
||||||
|
public const string Member = "Member";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ApprovalMembershipTargets
|
||||||
|
{
|
||||||
|
public const string Team = "Team";
|
||||||
|
public const string Client = "Client";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ApprovalStepConfigurationRules
|
||||||
|
{
|
||||||
|
public static readonly IReadOnlySet<string> AllowedTargetTypes = new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Role,
|
||||||
|
ApprovalStepTargetTypes.Membership,
|
||||||
|
ApprovalStepTargetTypes.Member,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly IReadOnlySet<string> AllowedRoleTargets = new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
KnownRoles.Administrator,
|
||||||
|
KnownRoles.Manager,
|
||||||
|
KnownRoles.WorkspaceMember,
|
||||||
|
KnownRoles.Client,
|
||||||
|
KnownRoles.Provider,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly IReadOnlySet<string> AllowedMembershipTargets = new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
ApprovalMembershipTargets.Team,
|
||||||
|
ApprovalMembershipTargets.Client,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static bool IsValidTargetType(string? targetType)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(targetType) && AllowedTargetTypes.Contains(targetType.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidRoleTarget(string? targetValue)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(targetValue) && AllowedRoleTargets.Contains(targetValue.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsValidMembershipTarget(string? targetValue)
|
||||||
|
{
|
||||||
|
return !string.IsNullOrWhiteSpace(targetValue) && AllowedMembershipTargets.Contains(targetValue.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
|
internal static class ApprovalModes
|
||||||
|
{
|
||||||
|
public const string None = "None";
|
||||||
|
public const string Optional = "Optional";
|
||||||
|
public const string Required = "Required";
|
||||||
|
public const string MultiLevel = "Multi-level";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class ApprovalWorkflowRules
|
||||||
|
{
|
||||||
|
public static bool BlocksManualApprovedOrScheduledStatus(string approvalMode)
|
||||||
|
{
|
||||||
|
return approvalMode is ApprovalModes.Required or ApprovalModes.MultiLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsApprovalCompletionStatus(string status)
|
||||||
|
{
|
||||||
|
return status is "Approved" or "Scheduled";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetFinalApprovalStatus(bool schedulePostsAutomaticallyOnApproval, DateTimeOffset? plannedPublishDate)
|
||||||
|
{
|
||||||
|
return schedulePostsAutomaticallyOnApproval && plannedPublishDate.HasValue
|
||||||
|
? "Scheduled"
|
||||||
|
: "Approved";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool HasRequiredStepApprovals(int approvedDecisionCount, int requiredApproverCount)
|
||||||
|
{
|
||||||
|
return approvedDecisionCount >= Math.Max(1, requiredApproverCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool CanApproveWorkflowStep(
|
||||||
|
bool isAdministrator,
|
||||||
|
bool hasWorkspaceAccess,
|
||||||
|
IReadOnlyCollection<string> userRoles,
|
||||||
|
Guid userId,
|
||||||
|
string? targetType,
|
||||||
|
string? targetValue)
|
||||||
|
{
|
||||||
|
if (isAdministrator)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasWorkspaceAccess ||
|
||||||
|
string.IsNullOrWhiteSpace(targetType) ||
|
||||||
|
string.IsNullOrWhiteSpace(targetValue))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetType switch
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Role => userRoles.Contains(targetValue),
|
||||||
|
ApprovalStepTargetTypes.Membership => MatchesMembershipTarget(userRoles, targetValue),
|
||||||
|
ApprovalStepTargetTypes.Member => ParseMemberTargetIds(targetValue).Contains(userId),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyCollection<Guid> ParseMemberTargetIds(string? targetValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(targetValue))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetValue
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
|
.Select(value => Guid.TryParse(value, out Guid memberUserId) ? memberUserId : Guid.Empty)
|
||||||
|
.Where(memberUserId => memberUserId != Guid.Empty)
|
||||||
|
.Distinct()
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatMemberTargetValue(IEnumerable<Guid> memberUserIds)
|
||||||
|
{
|
||||||
|
return string.Join(",", memberUserIds.Distinct().OrderBy(memberUserId => memberUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesMembershipTarget(
|
||||||
|
IReadOnlyCollection<string> userRoles,
|
||||||
|
string targetValue)
|
||||||
|
{
|
||||||
|
return targetValue switch
|
||||||
|
{
|
||||||
|
ApprovalMembershipTargets.Client => userRoles.Contains(KnownRoles.Client),
|
||||||
|
ApprovalMembershipTargets.Team => !userRoles.Contains(KnownRoles.Client),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Data;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Approvals.Data;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
using Socialize.Api.Modules.Notifications.Contracts;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Approvals.Services;
|
||||||
|
|
||||||
|
internal record ApprovalWorkflowStartResult(bool Succeeded, string? ErrorMessage);
|
||||||
|
|
||||||
|
internal record ApprovalWorkflowDecisionResult(
|
||||||
|
bool Succeeded,
|
||||||
|
string? ErrorMessage,
|
||||||
|
int StatusCode,
|
||||||
|
bool IsWorkflowStep);
|
||||||
|
|
||||||
|
internal class ApprovalWorkflowRuntimeService(
|
||||||
|
AppDbContext dbContext,
|
||||||
|
INotificationEventWriter notificationEventWriter)
|
||||||
|
{
|
||||||
|
private const string PendingState = "Pending";
|
||||||
|
private const string ApprovedState = "Approved";
|
||||||
|
|
||||||
|
public async Task<ApprovalWorkflowStartResult> StartMultiLevelWorkflowAsync(
|
||||||
|
ContentItem contentItem,
|
||||||
|
Workspace workspace,
|
||||||
|
Guid requestedByUserId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (workspace.ApprovalMode != ApprovalModes.MultiLevel)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowStartResult(false, "The workspace is not configured for multi-level approval.");
|
||||||
|
}
|
||||||
|
|
||||||
|
ApprovalWorkflowInstance? activeWorkflow = await dbContext.ApprovalWorkflowInstances
|
||||||
|
.SingleOrDefaultAsync(
|
||||||
|
workflow => workflow.ContentItemId == contentItem.Id && workflow.State == PendingState,
|
||||||
|
ct);
|
||||||
|
if (activeWorkflow is not null)
|
||||||
|
{
|
||||||
|
contentItem.Status = "In approval";
|
||||||
|
return new ApprovalWorkflowStartResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<WorkspaceApprovalStepConfiguration> configuredSteps = await dbContext.WorkspaceApprovalStepConfigurations
|
||||||
|
.Where(step => step.WorkspaceId == workspace.Id)
|
||||||
|
.OrderBy(step => step.SortOrder)
|
||||||
|
.ThenBy(step => step.Name)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
|
||||||
|
if (configuredSteps.Count == 0)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowStartResult(false, "Multi-level approval requires at least one configured approval step.");
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
|
var workflowInstance = new ApprovalWorkflowInstance
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
WorkspaceId = workspace.Id,
|
||||||
|
ContentItemId = contentItem.Id,
|
||||||
|
State = PendingState,
|
||||||
|
ApprovalMode = workspace.ApprovalMode,
|
||||||
|
StartedAt = now,
|
||||||
|
};
|
||||||
|
|
||||||
|
List<ApprovalRequest> workflowSteps = configuredSteps
|
||||||
|
.Select((step, index) => new ApprovalRequest
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
WorkspaceId = workspace.Id,
|
||||||
|
ContentItemId = contentItem.Id,
|
||||||
|
WorkflowInstanceId = workflowInstance.Id,
|
||||||
|
WorkflowStepSortOrder = index,
|
||||||
|
WorkflowStepTargetType = step.TargetType,
|
||||||
|
WorkflowStepTargetValue = step.TargetValue,
|
||||||
|
WorkflowStepRequiredApproverCount = step.RequiredApproverCount,
|
||||||
|
Stage = step.Name,
|
||||||
|
ReviewerName = FormatStepTarget(step),
|
||||||
|
ReviewerEmail = string.Empty,
|
||||||
|
RequestedByUserId = requestedByUserId,
|
||||||
|
DueAt = contentItem.DueDate,
|
||||||
|
State = PendingState,
|
||||||
|
AccessToken = CreateAccessToken(),
|
||||||
|
SentAt = now,
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
dbContext.ApprovalWorkflowInstances.Add(workflowInstance);
|
||||||
|
dbContext.ApprovalRequests.AddRange(workflowSteps);
|
||||||
|
contentItem.Status = "In approval";
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
await NotifyCurrentStepApproversAsync(workflowSteps[0], contentItem, ct);
|
||||||
|
|
||||||
|
return new ApprovalWorkflowStartResult(true, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApprovalWorkflowDecisionResult> ApplyWorkflowStepDecisionAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
ContentItem contentItem,
|
||||||
|
Workspace workspace,
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
ApprovalDecision decision,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (!approval.WorkflowInstanceId.HasValue)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(true, null, StatusCodes.Status200OK, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.Identity?.IsAuthenticated != true)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "Multi-level approval steps require an authenticated approver.", StatusCodes.Status401Unauthorized, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!await CanApproveStepAsync(user, approval, workspace.Id, ct))
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "You cannot approve the current workflow step.", StatusCodes.Status403Forbidden, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApprovalRequest? currentStep = await GetCurrentPendingStepAsync(approval.WorkflowInstanceId.Value, ct);
|
||||||
|
if (currentStep?.Id != approval.Id)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "Only the current pending approval step can be approved.", StatusCodes.Status409Conflict, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Guid currentUserId = user.GetUserId();
|
||||||
|
bool alreadyApproved = await dbContext.ApprovalDecisions.AnyAsync(
|
||||||
|
candidate => candidate.ApprovalRequestId == approval.Id &&
|
||||||
|
candidate.DecidedByUserId == currentUserId &&
|
||||||
|
candidate.Decision == ApprovedState,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
if (alreadyApproved)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "You have already approved this workflow step.", StatusCodes.Status409Conflict, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
dbContext.ApprovalDecisions.Add(decision);
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
var approvalDecisionParticipants = await dbContext.ApprovalDecisions
|
||||||
|
.Where(candidate => candidate.ApprovalRequestId == approval.Id && candidate.Decision == ApprovedState)
|
||||||
|
.Select(candidate => candidate.DecidedByUserId.HasValue
|
||||||
|
? candidate.DecidedByUserId.Value.ToString()
|
||||||
|
: candidate.DecidedByEmail)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
int approvedCount = approvalDecisionParticipants
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Count();
|
||||||
|
|
||||||
|
int requiredApproverCount = approval.WorkflowStepRequiredApproverCount ?? 1;
|
||||||
|
if (!ApprovalWorkflowRules.HasRequiredStepApprovals(approvedCount, requiredApproverCount))
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(true, null, StatusCodes.Status200OK, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
approval.State = ApprovedState;
|
||||||
|
approval.CompletedAt = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
|
ApprovalRequest? nextStep = await dbContext.ApprovalRequests
|
||||||
|
.Where(candidate => candidate.WorkflowInstanceId == approval.WorkflowInstanceId &&
|
||||||
|
candidate.State == PendingState &&
|
||||||
|
candidate.Id != approval.Id)
|
||||||
|
.OrderBy(candidate => candidate.WorkflowStepSortOrder)
|
||||||
|
.ThenBy(candidate => candidate.SentAt)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
|
||||||
|
if (nextStep is null)
|
||||||
|
{
|
||||||
|
ApprovalWorkflowInstance? workflowInstance = await dbContext.ApprovalWorkflowInstances
|
||||||
|
.SingleOrDefaultAsync(candidate => candidate.Id == approval.WorkflowInstanceId.Value, ct);
|
||||||
|
if (workflowInstance is null)
|
||||||
|
{
|
||||||
|
return new ApprovalWorkflowDecisionResult(false, "The approval workflow instance could not be found.", StatusCodes.Status404NotFound, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowInstance.State = ApprovedState;
|
||||||
|
workflowInstance.CompletedAt = DateTimeOffset.UtcNow;
|
||||||
|
contentItem.Status = ApprovalWorkflowRules.GetFinalApprovalStatus(
|
||||||
|
workspace.SchedulePostsAutomaticallyOnApproval,
|
||||||
|
contentItem.DueDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync(ct);
|
||||||
|
|
||||||
|
if (nextStep is null)
|
||||||
|
{
|
||||||
|
await NotifyPublishUsersAsync(approval, contentItem, ct);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await NotifyCurrentStepApproversAsync(nextStep, contentItem, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ApprovalWorkflowDecisionResult(true, null, StatusCodes.Status200OK, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HasCompletedMultiLevelWorkflowAsync(Guid contentItemId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await dbContext.ApprovalWorkflowInstances.AnyAsync(
|
||||||
|
workflow => workflow.ContentItemId == contentItemId && workflow.State == ApprovedState,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ApprovalRequest?> GetCurrentPendingStepAsync(Guid workflowInstanceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await dbContext.ApprovalRequests
|
||||||
|
.Where(candidate => candidate.WorkflowInstanceId == workflowInstanceId && candidate.State == PendingState)
|
||||||
|
.OrderBy(candidate => candidate.WorkflowStepSortOrder)
|
||||||
|
.ThenBy(candidate => candidate.SentAt)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> CanApproveStepAsync(
|
||||||
|
ClaimsPrincipal user,
|
||||||
|
ApprovalRequest approval,
|
||||||
|
Guid workspaceId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
Guid userId = user.GetUserId();
|
||||||
|
bool hasWorkspaceAccess = await UserHasWorkspaceAccessAsync(userId, workspaceId, ct);
|
||||||
|
string[] userRoles = ApprovalStepConfigurationRules.AllowedRoleTargets
|
||||||
|
.Where(user.IsInRole)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
return ApprovalWorkflowRules.CanApproveWorkflowStep(
|
||||||
|
user.IsInRole(KnownRoles.Administrator),
|
||||||
|
hasWorkspaceAccess,
|
||||||
|
userRoles,
|
||||||
|
userId,
|
||||||
|
approval.WorkflowStepTargetType,
|
||||||
|
approval.WorkflowStepTargetValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> UserHasWorkspaceAccessAsync(Guid userId, Guid workspaceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
string workspaceClaimValue = workspaceId.ToString();
|
||||||
|
return await dbContext.UserClaims.AnyAsync(
|
||||||
|
claim => claim.UserId == userId &&
|
||||||
|
claim.ClaimType == KnownClaims.WorkspaceScope &&
|
||||||
|
claim.ClaimValue == workspaceClaimValue,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NotifyCurrentStepApproversAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
ContentItem contentItem,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
List<ApprovalNotificationRecipient> recipients = await GetStepApproverRecipientsAsync(approval, ct);
|
||||||
|
|
||||||
|
foreach (ApprovalNotificationRecipient recipient in recipients)
|
||||||
|
{
|
||||||
|
await notificationEventWriter.WriteAsync(
|
||||||
|
new NotificationEventWriteModel(
|
||||||
|
approval.WorkspaceId,
|
||||||
|
approval.ContentItemId,
|
||||||
|
"approval.step.current",
|
||||||
|
"ApprovalRequest",
|
||||||
|
approval.Id,
|
||||||
|
$"{approval.Stage} approval is ready for {contentItem.Title}.",
|
||||||
|
recipient.UserId,
|
||||||
|
recipient.Email,
|
||||||
|
$$"""{"stage":"{{approval.Stage}}","requiredApproverCount":{{approval.WorkflowStepRequiredApproverCount ?? 1}}}"""),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task NotifyPublishUsersAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
ContentItem contentItem,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
List<ApprovalNotificationRecipient> recipients = await GetPublishRecipientUsersAsync(approval.WorkspaceId, ct);
|
||||||
|
|
||||||
|
foreach (ApprovalNotificationRecipient recipient in recipients)
|
||||||
|
{
|
||||||
|
await notificationEventWriter.WriteAsync(
|
||||||
|
new NotificationEventWriteModel(
|
||||||
|
approval.WorkspaceId,
|
||||||
|
approval.ContentItemId,
|
||||||
|
"approval.workflow.completed",
|
||||||
|
"ApprovalWorkflowInstance",
|
||||||
|
approval.WorkflowInstanceId!.Value,
|
||||||
|
$"Final approval completed for {contentItem.Title}.",
|
||||||
|
recipient.UserId,
|
||||||
|
recipient.Email,
|
||||||
|
$$"""{"status":"{{contentItem.Status}}"}"""),
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetStepApproverRecipientsAsync(
|
||||||
|
ApprovalRequest approval,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
string? targetType = approval.WorkflowStepTargetType;
|
||||||
|
string? targetValue = approval.WorkflowStepTargetValue;
|
||||||
|
if (string.IsNullOrWhiteSpace(targetType) || string.IsNullOrWhiteSpace(targetValue))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetType switch
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Member => await GetMemberRecipientsAsync(targetValue, ct),
|
||||||
|
ApprovalStepTargetTypes.Role => await GetRoleRecipientsAsync(approval.WorkspaceId, [targetValue], ct),
|
||||||
|
ApprovalStepTargetTypes.Membership => await GetMembershipRecipientsAsync(approval.WorkspaceId, targetValue, ct),
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetMemberRecipientsAsync(string targetValue, CancellationToken ct)
|
||||||
|
{
|
||||||
|
IReadOnlyCollection<Guid> userIds = ApprovalWorkflowRules.ParseMemberTargetIds(targetValue);
|
||||||
|
if (userIds.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return await dbContext.Users
|
||||||
|
.Where(user => userIds.Contains(user.Id))
|
||||||
|
.Select(user => new ApprovalNotificationRecipient(user.Id, user.Email))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetMembershipRecipientsAsync(
|
||||||
|
Guid workspaceId,
|
||||||
|
string targetValue,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
string[] roles = targetValue switch
|
||||||
|
{
|
||||||
|
ApprovalMembershipTargets.Client => [KnownRoles.Client],
|
||||||
|
ApprovalMembershipTargets.Team => [KnownRoles.Administrator, KnownRoles.Manager, KnownRoles.WorkspaceMember, KnownRoles.Provider],
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return roles.Length == 0
|
||||||
|
? []
|
||||||
|
: await GetRoleRecipientsAsync(workspaceId, roles, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetPublishRecipientUsersAsync(Guid workspaceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await GetRoleRecipientsAsync(workspaceId, [KnownRoles.Administrator, KnownRoles.Manager], ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<ApprovalNotificationRecipient>> GetRoleRecipientsAsync(
|
||||||
|
Guid workspaceId,
|
||||||
|
IReadOnlyCollection<string> roles,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
string workspaceClaimValue = workspaceId.ToString();
|
||||||
|
|
||||||
|
return await dbContext.UserRoles
|
||||||
|
.Join(
|
||||||
|
dbContext.Roles,
|
||||||
|
userRole => userRole.RoleId,
|
||||||
|
role => role.Id,
|
||||||
|
(userRole, role) => new { userRole.UserId, RoleName = role.Name })
|
||||||
|
.Where(candidate => candidate.RoleName != null && roles.Contains(candidate.RoleName))
|
||||||
|
.Join(
|
||||||
|
dbContext.UserClaims.Where(claim =>
|
||||||
|
claim.ClaimType == KnownClaims.WorkspaceScope &&
|
||||||
|
claim.ClaimValue == workspaceClaimValue),
|
||||||
|
candidate => candidate.UserId,
|
||||||
|
claim => claim.UserId,
|
||||||
|
(candidate, _) => candidate.UserId)
|
||||||
|
.Distinct()
|
||||||
|
.Join(
|
||||||
|
dbContext.Users,
|
||||||
|
userId => userId,
|
||||||
|
user => user.Id,
|
||||||
|
(_, user) => new ApprovalNotificationRecipient(user.Id, user.Email))
|
||||||
|
.ToListAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatStepTarget(WorkspaceApprovalStepConfiguration step)
|
||||||
|
{
|
||||||
|
return step.TargetType switch
|
||||||
|
{
|
||||||
|
ApprovalStepTargetTypes.Role => $"Role: {step.TargetValue}",
|
||||||
|
ApprovalStepTargetTypes.Membership => $"Membership: {step.TargetValue}",
|
||||||
|
ApprovalStepTargetTypes.Member => "Assigned members",
|
||||||
|
_ => step.TargetValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateAccessToken()
|
||||||
|
{
|
||||||
|
return Convert.ToHexString(RandomNumberGenerator.GetBytes(16));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record ApprovalNotificationRecipient(Guid UserId, string? Email);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Assets.Data;
|
namespace Socialize.Api.Modules.Assets.Data;
|
||||||
|
|
||||||
public class Asset
|
internal class Asset
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid WorkspaceId { get; set; }
|
public Guid WorkspaceId { get; set; }
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Socialize.Api.Modules.ContentItems.Data;
|
||||||
|
using Socialize.Api.Modules.Workspaces.Data;
|
||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets.Data;
|
namespace Socialize.Api.Modules.Assets.Data;
|
||||||
|
|
||||||
public static class AssetModelConfiguration
|
internal static class AssetModelConfiguration
|
||||||
{
|
{
|
||||||
public static ModelBuilder ConfigureAssetsModule(this ModelBuilder modelBuilder)
|
public static ModelBuilder ConfigureAssetsModule(this ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -21,6 +23,14 @@ public static class AssetModelConfiguration
|
|||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
asset.HasIndex(x => x.WorkspaceId);
|
asset.HasIndex(x => x.WorkspaceId);
|
||||||
asset.HasIndex(x => x.ContentItemId);
|
asset.HasIndex(x => x.ContentItemId);
|
||||||
|
asset.HasOne<Workspace>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.WorkspaceId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
asset.HasOne<ContentItem>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ContentItemId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<AssetRevision>(revision =>
|
modelBuilder.Entity<AssetRevision>(revision =>
|
||||||
@@ -35,6 +45,10 @@ public static class AssetModelConfiguration
|
|||||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||||
revision.HasIndex(x => x.AssetId);
|
revision.HasIndex(x => x.AssetId);
|
||||||
revision.HasIndex(x => new { x.AssetId, x.RevisionNumber }).IsUnique();
|
revision.HasIndex(x => new { x.AssetId, x.RevisionNumber }).IsUnique();
|
||||||
|
revision.HasOne<Asset>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.AssetId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
});
|
});
|
||||||
|
|
||||||
return modelBuilder;
|
return modelBuilder;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Socialize.Api.Modules.Assets.Data;
|
namespace Socialize.Api.Modules.Assets.Data;
|
||||||
|
|
||||||
public class AssetRevision
|
internal class AssetRevision
|
||||||
{
|
{
|
||||||
public Guid Id { get; init; }
|
public Guid Id { get; init; }
|
||||||
public Guid AssetId { get; set; }
|
public Guid AssetId { get; set; }
|
||||||
|
|||||||
@@ -3,17 +3,19 @@ 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;
|
||||||
|
|
||||||
public record CreateAssetRevisionRequest(
|
internal record CreateAssetRevisionRequest(
|
||||||
string SourceReference,
|
string SourceReference,
|
||||||
string? PreviewUrl,
|
string? PreviewUrl,
|
||||||
string? Notes);
|
string? Notes);
|
||||||
|
|
||||||
public class CreateAssetRevisionRequestValidator
|
internal class CreateAssetRevisionRequestValidator
|
||||||
: Validator<CreateAssetRevisionRequest>
|
: Validator<CreateAssetRevisionRequest>
|
||||||
{
|
{
|
||||||
public CreateAssetRevisionRequestValidator()
|
public CreateAssetRevisionRequestValidator()
|
||||||
@@ -24,9 +26,10 @@ public class CreateAssetRevisionRequestValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateAssetRevisionHandler(
|
internal 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.CanContributeToProject(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
|
!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,12 +3,14 @@ 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;
|
||||||
|
|
||||||
public record CreateGoogleDriveAssetRequest(
|
internal record CreateGoogleDriveAssetRequest(
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ContentItemId,
|
Guid ContentItemId,
|
||||||
string AssetType,
|
string AssetType,
|
||||||
@@ -17,7 +19,7 @@ public record CreateGoogleDriveAssetRequest(
|
|||||||
string GoogleDriveLink,
|
string GoogleDriveLink,
|
||||||
string? PreviewUrl);
|
string? PreviewUrl);
|
||||||
|
|
||||||
public class CreateGoogleDriveAssetRequestValidator
|
internal class CreateGoogleDriveAssetRequestValidator
|
||||||
: Validator<CreateGoogleDriveAssetRequest>
|
: Validator<CreateGoogleDriveAssetRequest>
|
||||||
{
|
{
|
||||||
public CreateGoogleDriveAssetRequestValidator()
|
public CreateGoogleDriveAssetRequestValidator()
|
||||||
@@ -32,9 +34,10 @@ public class CreateGoogleDriveAssetRequestValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateGoogleDriveAssetHandler(
|
internal 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.CanContributeToProject(User, contentItem.WorkspaceId, contentItem.ClientId, contentItem.ProjectId))
|
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,
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ using Socialize.Api.Infrastructure.Security;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets.Handlers;
|
namespace Socialize.Api.Modules.Assets.Handlers;
|
||||||
|
|
||||||
public record GetAssetsRequest(Guid ContentItemId);
|
internal record GetAssetsRequest(Guid ContentItemId);
|
||||||
|
|
||||||
public record AssetRevisionDto(
|
internal record AssetRevisionDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid AssetId,
|
Guid AssetId,
|
||||||
int RevisionNumber,
|
int RevisionNumber,
|
||||||
@@ -17,7 +17,7 @@ public record AssetRevisionDto(
|
|||||||
Guid? CreatedByUserId,
|
Guid? CreatedByUserId,
|
||||||
DateTimeOffset CreatedAt);
|
DateTimeOffset CreatedAt);
|
||||||
|
|
||||||
public record AssetDto(
|
internal record AssetDto(
|
||||||
Guid Id,
|
Guid Id,
|
||||||
Guid WorkspaceId,
|
Guid WorkspaceId,
|
||||||
Guid ContentItemId,
|
Guid ContentItemId,
|
||||||
@@ -31,7 +31,7 @@ public record AssetDto(
|
|||||||
DateTimeOffset CreatedAt,
|
DateTimeOffset CreatedAt,
|
||||||
IReadOnlyCollection<AssetRevisionDto> Revisions);
|
IReadOnlyCollection<AssetRevisionDto> Revisions);
|
||||||
|
|
||||||
public class GetAssetsHandler(
|
internal class GetAssetsHandler(
|
||||||
AppDbContext dbContext,
|
AppDbContext dbContext,
|
||||||
AccessScopeService accessScopeService)
|
AccessScopeService accessScopeService)
|
||||||
: Endpoint<GetAssetsRequest, IReadOnlyCollection<AssetDto>>
|
: Endpoint<GetAssetsRequest, IReadOnlyCollection<AssetDto>>
|
||||||
@@ -52,7 +52,7 @@ public class GetAssetsHandler(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!accessScopeService.CanReviewContent(User, item.WorkspaceId, item.ClientId, item.ProjectId))
|
if (!await accessScopeService.CanReviewContentAsync(User, item.WorkspaceId, item.ClientId, item.CampaignId, ct))
|
||||||
{
|
{
|
||||||
await SendForbiddenAsync(ct);
|
await SendForbiddenAsync(ct);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using Socialize.Api.Modules.Assets.Data;
|
|||||||
|
|
||||||
namespace Socialize.Api.Modules.Assets;
|
namespace Socialize.Api.Modules.Assets;
|
||||||
|
|
||||||
public static class DependencyInjection
|
internal static class ModuleRegistration
|
||||||
{
|
{
|
||||||
public static WebApplicationBuilder AddAssetsModule(
|
public static WebApplicationBuilder AddAssetsModule(
|
||||||
this WebApplicationBuilder builder)
|
this WebApplicationBuilder builder)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
|
||||||
|
internal 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,55 @@
|
|||||||
|
namespace Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
|
||||||
|
#pragma warning disable S1075 // Catalog seed entries intentionally store source URLs.
|
||||||
|
|
||||||
|
internal 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;
|
||||||
|
|
||||||
|
internal 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;
|
||||||
|
|
||||||
|
internal 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;
|
||||||
|
|
||||||
|
internal 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;
|
||||||
|
|
||||||
|
internal 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,112 @@
|
|||||||
|
using Socialize.Api.Modules.CalendarIntegrations.Data;
|
||||||
|
using Socialize.Api.Modules.CalendarIntegrations.Services;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.CalendarIntegrations.Handlers;
|
||||||
|
|
||||||
|
internal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal record UpsertCalendarSourceRequest(
|
||||||
|
string Scope,
|
||||||
|
Guid? OrganizationId,
|
||||||
|
Guid? WorkspaceId,
|
||||||
|
string? SourceUrl,
|
||||||
|
string? CatalogSourceReference,
|
||||||
|
string DisplayTitle,
|
||||||
|
string Color,
|
||||||
|
string Category,
|
||||||
|
bool IsEnabled,
|
||||||
|
string? InheritanceMode);
|
||||||
|
|
||||||
|
internal 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;
|
||||||
|
|
||||||
|
internal 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 &&
|
||||||
|
EF.Functions.ILike(source.SourceUrl, normalizedUrl)),
|
||||||
|
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;
|
||||||
|
|
||||||
|
internal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user