mirror of
https://github.com/bitwarden/server.git
synced 2025-01-30 23:11:22 +01:00
Merge branch 'main' into ac/ac-1682/ef-migrations
This commit is contained in:
commit
6c21d4e96a
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
@ -49,6 +49,7 @@
|
|||||||
"Azure.Messaging.ServiceBus",
|
"Azure.Messaging.ServiceBus",
|
||||||
"Azure.Storage.Blobs",
|
"Azure.Storage.Blobs",
|
||||||
"Azure.Storage.Queues",
|
"Azure.Storage.Queues",
|
||||||
|
"DuoUniversal",
|
||||||
"Fido2.AspNet",
|
"Fido2.AspNet",
|
||||||
"Duende.IdentityServer",
|
"Duende.IdentityServer",
|
||||||
"Microsoft.Azure.Cosmos",
|
"Microsoft.Azure.Cosmos",
|
||||||
|
33
.github/workflows/build.yml
vendored
33
.github/workflows/build.yml
vendored
@ -540,36 +540,11 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: |
|
if: |
|
||||||
github.ref == 'refs/heads/main'
|
(github.ref == 'refs/heads/main'
|
||||||
|| github.ref == 'refs/heads/rc'
|
|| github.ref == 'refs/heads/rc'
|
||||||
|| github.ref == 'refs/heads/hotfix-rc'
|
|| github.ref == 'refs/heads/hotfix-rc')
|
||||||
env:
|
&& contains(needs.*.result, 'failure')
|
||||||
LINT_STATUS: ${{ needs.lint.result }}
|
run: exit 1
|
||||||
TESTING_STATUS: ${{ needs.testing.result }}
|
|
||||||
BUILD_ARTIFACTS_STATUS: ${{ needs.build-artifacts.result }}
|
|
||||||
BUILD_DOCKER_STATUS: ${{ needs.build-docker.result }}
|
|
||||||
UPLOAD_STATUS: ${{ needs.upload.result }}
|
|
||||||
BUILD_MSSQLMIGRATORUTILITY_STATUS: ${{ needs.build-mssqlmigratorutility.result }}
|
|
||||||
TRIGGER_SELF_HOST_BUILD_STATUS: ${{ needs.self-host-build.result }}
|
|
||||||
TRIGGER_K8S_DEPLOY_STATUS: ${{ needs.trigger-k8s-deploy.result }}
|
|
||||||
run: |
|
|
||||||
if [ "$LINT_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
|
||||||
elif [ "$TESTING_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
|
||||||
elif [ "$BUILD_ARTIFACTS_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
|
||||||
elif [ "$BUILD_DOCKER_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
|
||||||
elif [ "$UPLOAD_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
|
||||||
elif [ "$BUILD_MSSQLMIGRATORUTILITY_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
|
||||||
elif [ "$TRIGGER_SELF_HOST_BUILD_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
|
||||||
elif [ "$TRIGGER_K8S_DEPLOY_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Log in to Azure - CI subscription
|
- name: Log in to Azure - CI subscription
|
||||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||||
|
53
.github/workflows/cleanup-rc-branch.yml
vendored
Normal file
53
.github/workflows/cleanup-rc-branch.yml
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: Cleanup RC Branch
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v**
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
delete-rc:
|
||||||
|
name: Delete RC Branch
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Login to Azure - CI Subscription
|
||||||
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
|
- name: Retrieve bot secrets
|
||||||
|
id: retrieve-bot-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
with:
|
||||||
|
keyvault: bitwarden-ci
|
||||||
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
- name: Checkout main
|
||||||
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
|
||||||
|
- name: Check if a RC branch exists
|
||||||
|
id: branch-check
|
||||||
|
run: |
|
||||||
|
hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)
|
||||||
|
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
|
||||||
|
|
||||||
|
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
|
||||||
|
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${rc_branch_check}" -gt 0 ]]; then
|
||||||
|
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
echo "name=rc" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Delete RC branch
|
||||||
|
env:
|
||||||
|
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
|
||||||
|
run: |
|
||||||
|
if ! [[ -z "$BRANCH_NAME" ]]; then
|
||||||
|
git push --quiet origin --delete $BRANCH_NAME
|
||||||
|
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
15
.github/workflows/container-registry-purge.yml
vendored
15
.github/workflows/container-registry-purge.yml
vendored
@ -69,20 +69,15 @@ jobs:
|
|||||||
name: Check for failures
|
name: Check for failures
|
||||||
if: always()
|
if: always()
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs: [purge]
|
||||||
- purge
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: |
|
if: |
|
||||||
github.ref == 'refs/heads/main'
|
(github.ref == 'refs/heads/main'
|
||||||
|| github.ref == 'refs/heads/rc'
|
|| github.ref == 'refs/heads/rc'
|
||||||
|| github.ref == 'refs/heads/hotfix-rc'
|
|| github.ref == 'refs/heads/hotfix-rc')
|
||||||
env:
|
&& contains(needs.*.result, 'failure')
|
||||||
PURGE_STATUS: ${{ needs.purge.result }}
|
run: exit 1
|
||||||
run: |
|
|
||||||
if [ "$PURGE_STATUS" = "failure" ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Log in to Azure - CI subscription
|
- name: Log in to Azure - CI subscription
|
||||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||||
|
19
.github/workflows/scan.yml
vendored
19
.github/workflows/scan.yml
vendored
@ -7,25 +7,33 @@ on:
|
|||||||
- "main"
|
- "main"
|
||||||
- "rc"
|
- "rc"
|
||||||
- "hotfix-rc"
|
- "hotfix-rc"
|
||||||
pull_request:
|
pull_request_target:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
|
||||||
permissions: read-all
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
check-run:
|
||||||
|
name: Check PR run
|
||||||
|
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||||
|
|
||||||
sast:
|
sast:
|
||||||
name: SAST scan
|
name: SAST scan
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
needs: check-run
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write
|
security-events: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Scan with Checkmarx
|
- name: Scan with Checkmarx
|
||||||
uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23
|
uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23
|
||||||
env:
|
env:
|
||||||
INCREMENTAL: "${{ github.event_name == 'pull_request' && '--sast-incremental' || '' }}"
|
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||||
with:
|
with:
|
||||||
project_name: ${{ github.repository }}
|
project_name: ${{ github.repository }}
|
||||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||||
@ -35,17 +43,21 @@ jobs:
|
|||||||
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
|
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
|
||||||
|
|
||||||
- name: Upload Checkmarx results to GitHub
|
- name: Upload Checkmarx results to GitHub
|
||||||
uses: github/codeql-action/upload-sarif@b7bf0a3ed3ecfa44160715d7c442788f65f0f923 # v3.23.2
|
uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6
|
||||||
with:
|
with:
|
||||||
sarif_file: cx_result.sarif
|
sarif_file: cx_result.sarif
|
||||||
|
|
||||||
quality:
|
quality:
|
||||||
name: Quality scan
|
name: Quality scan
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
needs: check-run
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Scan with SonarCloud
|
- name: Scan with SonarCloud
|
||||||
uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1
|
uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1
|
||||||
@ -56,5 +68,4 @@ jobs:
|
|||||||
args: >
|
args: >
|
||||||
-Dsonar.organization=${{ github.repository_owner }}
|
-Dsonar.organization=${{ github.repository_owner }}
|
||||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||||
-Dsonar.test.exclusions=test/**
|
|
||||||
-Dsonar.tests=test/
|
-Dsonar.tests=test/
|
||||||
|
12
.github/workflows/test-database.yml
vendored
12
.github/workflows/test-database.yml
vendored
@ -57,9 +57,9 @@ jobs:
|
|||||||
run: sleep 15s
|
run: sleep 15s
|
||||||
|
|
||||||
- name: Migrate SQL Server
|
- name: Migrate SQL Server
|
||||||
working-directory: "dev"
|
run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
|
||||||
run: "./migrate.ps1"
|
env:
|
||||||
shell: pwsh
|
CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
|
||||||
|
|
||||||
- name: Migrate MySQL
|
- name: Migrate MySQL
|
||||||
working-directory: "util/MySqlMigrations"
|
working-directory: "util/MySqlMigrations"
|
||||||
@ -147,9 +147,9 @@ jobs:
|
|||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Migrate
|
- name: Migrate
|
||||||
working-directory: "dev"
|
run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
|
||||||
run: "./migrate.ps1"
|
env:
|
||||||
shell: pwsh
|
CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
|
||||||
|
|
||||||
- name: Diff .sqlproj to migrations
|
- name: Diff .sqlproj to migrations
|
||||||
run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
|
run: /usr/local/sqlpackage/sqlpackage /action:DeployReport /SourceFile:"Sql.dacpac" /TargetConnectionString:"Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;" /OutputPath:"report.xml" /p:IgnoreColumnOrder=True /p:IgnoreComments=True
|
||||||
|
116
.github/workflows/version-bump.yml
vendored
116
.github/workflows/version-bump.yml
vendored
@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
name: Bump version
|
name: Version Bump
|
||||||
run-name: Bump version to ${{ inputs.version_number }}
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version_number:
|
version_number_override:
|
||||||
description: "New version (example: '2024.1.0')"
|
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
||||||
required: true
|
required: false
|
||||||
type: string
|
type: string
|
||||||
cut_rc_branch:
|
cut_rc_branch:
|
||||||
description: "Cut RC branch?"
|
description: "Cut RC branch?"
|
||||||
@ -16,22 +15,16 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump_version:
|
bump_version:
|
||||||
name: Bump
|
name: Bump Version
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Log in to Azure - CI subscription
|
- name: Validate version input
|
||||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
if: ${{ inputs.version_number_override != '' }}
|
||||||
|
uses: bitwarden/gh-actions/version-check@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
|
||||||
id: retrieve-secrets
|
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
|
||||||
with:
|
|
||||||
keyvault: "bitwarden-ci"
|
|
||||||
secrets: "github-gpg-private-key,
|
|
||||||
github-gpg-private-key-passphrase,
|
|
||||||
github-pat-bitwarden-devops-bot-repo-scope"
|
|
||||||
|
|
||||||
- name: Check out branch
|
- name: Check out branch
|
||||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||||
@ -48,6 +41,20 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Log in to Azure - CI subscription
|
||||||
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
id: retrieve-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-ci"
|
||||||
|
secrets: "github-gpg-private-key,
|
||||||
|
github-gpg-private-key-passphrase,
|
||||||
|
github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
||||||
with:
|
with:
|
||||||
@ -56,22 +63,35 @@ jobs:
|
|||||||
git_user_signingkey: true
|
git_user_signingkey: true
|
||||||
git_commit_gpgsign: true
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
|
- name: Set up Git
|
||||||
|
run: |
|
||||||
|
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||||
|
git config --local user.name "bitwarden-devops-bot"
|
||||||
|
|
||||||
- name: Create version branch
|
- name: Create version branch
|
||||||
id: create-branch
|
id: create-branch
|
||||||
run: |
|
run: |
|
||||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
|
||||||
git switch -c $NAME
|
git switch -c $NAME
|
||||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Install xmllint
|
- name: Install xmllint
|
||||||
run: sudo apt install -y libxml2-utils
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libxml2-utils
|
||||||
|
|
||||||
- name: Verify input version
|
- name: Get current version
|
||||||
env:
|
id: current-version
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
|
||||||
run: |
|
run: |
|
||||||
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
|
||||||
|
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Verify input version
|
||||||
|
if: ${{ inputs.version_number_override != '' }}
|
||||||
|
env:
|
||||||
|
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
|
||||||
|
NEW_VERSION: ${{ inputs.version_number_override }}
|
||||||
|
run: |
|
||||||
# Error if version has not changed.
|
# Error if version has not changed.
|
||||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||||
echo "Version has not changed."
|
echo "Version has not changed."
|
||||||
@ -87,16 +107,37 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Bump version props
|
- name: Calculate next release version
|
||||||
|
if: ${{ inputs.version_number_override == '' }}
|
||||||
|
id: calculate-next-version
|
||||||
|
uses: bitwarden/gh-actions/version-next@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.current-version.outputs.version }}
|
||||||
|
|
||||||
|
- name: Bump version props - Version Override
|
||||||
|
if: ${{ inputs.version_number_override != '' }}
|
||||||
|
id: bump-version-override
|
||||||
uses: bitwarden/gh-actions/version-bump@main
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number }}
|
|
||||||
file_path: "Directory.Build.props"
|
file_path: "Directory.Build.props"
|
||||||
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
- name: Set up Git
|
- name: Bump version props - Automatic Calculation
|
||||||
|
if: ${{ inputs.version_number_override == '' }}
|
||||||
|
id: bump-version-automatic
|
||||||
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
|
with:
|
||||||
|
file_path: "Directory.Build.props"
|
||||||
|
version: ${{ steps.calculate-next-version.outputs.version }}
|
||||||
|
|
||||||
|
- name: Set final version output
|
||||||
|
id: set-final-version-output
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
|
||||||
git config --local user.name "bitwarden-devops-bot"
|
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
|
||||||
|
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Check if version changed
|
- name: Check if version changed
|
||||||
id: version-changed
|
id: version-changed
|
||||||
@ -110,7 +151,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Commit files
|
- name: Commit files
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
run: git commit -m "Bumped version to ${{ inputs.version_number }}" -a
|
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
@ -124,7 +165,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||||
TITLE: "Bump version to ${{ inputs.version_number }}"
|
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
|
||||||
run: |
|
run: |
|
||||||
PR_URL=$(gh pr create --title "$TITLE" \
|
PR_URL=$(gh pr create --title "$TITLE" \
|
||||||
--base "main" \
|
--base "main" \
|
||||||
@ -140,38 +181,43 @@ jobs:
|
|||||||
- [X] Other
|
- [X] Other
|
||||||
|
|
||||||
## Objective
|
## Objective
|
||||||
Automated version bump to ${{ inputs.version_number }}")
|
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
|
||||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Approve PR
|
- name: Approve PR
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
run: gh pr review $PR_NUMBER --approve
|
run: gh pr review $PR_NUMBER --approve
|
||||||
|
|
||||||
- name: Merge PR
|
- name: Merge PR
|
||||||
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||||
|
|
||||||
|
|
||||||
cut_rc:
|
cut_rc:
|
||||||
name: Cut RC branch
|
name: Cut RC branch
|
||||||
needs: bump_version
|
|
||||||
if: ${{ inputs.cut_rc_branch == true }}
|
if: ${{ inputs.cut_rc_branch == true }}
|
||||||
|
needs: bump_version
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out branch
|
- name: Check out branch
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Install xmllint
|
- name: Install xmllint
|
||||||
run: sudo apt install -y libxml2-utils
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libxml2-utils
|
||||||
|
|
||||||
- name: Verify version has been updated
|
- name: Verify version has been updated
|
||||||
env:
|
env:
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
# Wait for version to change.
|
# Wait for version to change.
|
||||||
while : ; do
|
while : ; do
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2024.2.3</Version>
|
<Version>2024.3.0</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -17,6 +17,7 @@ using Bit.Core.Services;
|
|||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
namespace Bit.Commercial.Core.AdminConsole.Services;
|
namespace Bit.Commercial.Core.AdminConsole.Services;
|
||||||
|
|
||||||
@ -257,7 +258,7 @@ public class ProviderService : IProviderService
|
|||||||
|
|
||||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||||
events.Add((providerUser, EventType.ProviderUser_Confirmed, null));
|
events.Add((providerUser, EventType.ProviderUser_Confirmed, null));
|
||||||
await _mailService.SendProviderConfirmedEmailAsync(provider.Name, user.Email);
|
await _mailService.SendProviderConfirmedEmailAsync(provider.DisplayName(), user.Email);
|
||||||
result.Add(Tuple.Create(providerUser, ""));
|
result.Add(Tuple.Create(providerUser, ""));
|
||||||
}
|
}
|
||||||
catch (BadRequestException e)
|
catch (BadRequestException e)
|
||||||
@ -331,7 +332,7 @@ public class ProviderService : IProviderService
|
|||||||
var email = user == null ? providerUser.Email : user.Email;
|
var email = user == null ? providerUser.Email : user.Email;
|
||||||
if (!string.IsNullOrWhiteSpace(email))
|
if (!string.IsNullOrWhiteSpace(email))
|
||||||
{
|
{
|
||||||
await _mailService.SendProviderUserRemoved(provider.Name, email);
|
await _mailService.SendProviderUserRemoved(provider.DisplayName(), email);
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Add(Tuple.Create(providerUser, ""));
|
result.Add(Tuple.Create(providerUser, ""));
|
||||||
@ -374,8 +375,18 @@ public class ProviderService : IProviderService
|
|||||||
Key = key,
|
Key = key,
|
||||||
};
|
};
|
||||||
|
|
||||||
await ApplyProviderPriceRateAsync(organizationId, providerId);
|
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||||
|
|
||||||
|
await ApplyProviderPriceRateAsync(organization, provider);
|
||||||
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
||||||
|
|
||||||
|
organization.BillingEmail = provider.BillingEmail;
|
||||||
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||||
|
{
|
||||||
|
Email = provider.BillingEmail
|
||||||
|
});
|
||||||
|
|
||||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,16 +411,14 @@ public class ProviderService : IProviderService
|
|||||||
await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));
|
await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ApplyProviderPriceRateAsync(Guid organizationId, Guid providerId)
|
private async Task ApplyProviderPriceRateAsync(Organization organization, Provider provider)
|
||||||
{
|
{
|
||||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
|
||||||
// if a provider was created before Nov 6, 2023.If true, the organization plan assigned to that provider is updated to a 2020 plan.
|
// if a provider was created before Nov 6, 2023.If true, the organization plan assigned to that provider is updated to a 2020 plan.
|
||||||
if (provider.CreationDate >= Constants.ProviderCreatedPriorNov62023)
|
if (provider.CreationDate >= Constants.ProviderCreatedPriorNov62023)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, GetStripeSeatPlanId(organization.PlanType));
|
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, GetStripeSeatPlanId(organization.PlanType));
|
||||||
var extractedPlanType = PlanTypeMappings(organization);
|
var extractedPlanType = PlanTypeMappings(organization);
|
||||||
if (subscriptionItem != null)
|
if (subscriptionItem != null)
|
||||||
@ -586,7 +595,7 @@ public class ProviderService : IProviderService
|
|||||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||||
var token = _dataProtector.Protect(
|
var token = _dataProtector.Protect(
|
||||||
$"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}");
|
$"ProviderUserInvite {providerUser.Id} {providerUser.Email} {nowMillis}");
|
||||||
await _mailService.SendProviderInviteEmailAsync(provider.Name, providerUser, token, providerUser.Email);
|
await _mailService.SendProviderInviteEmailAsync(provider.DisplayName(), providerUser, token, providerUser.Email);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds)
|
private async Task<bool> HasConfirmedProviderAdminExceptAsync(Guid providerId, IEnumerable<Guid> providerUserIds)
|
||||||
|
@ -26,6 +26,7 @@ using IdentityModel;
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using AuthenticationSchemes = Bit.Core.AuthenticationSchemes;
|
||||||
using DIM = Duende.IdentityServer.Models;
|
using DIM = Duende.IdentityServer.Models;
|
||||||
|
|
||||||
namespace Bit.Sso.Controllers;
|
namespace Bit.Sso.Controllers;
|
||||||
@ -483,7 +484,7 @@ public class AccountController : Controller
|
|||||||
if (orgUser.Status == OrganizationUserStatusType.Invited)
|
if (orgUser.Status == OrganizationUserStatusType.Invited)
|
||||||
{
|
{
|
||||||
// Org User is invited - they must manually accept the invite via email and authenticate with MP
|
// Org User is invited - they must manually accept the invite via email and authenticate with MP
|
||||||
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.Name));
|
throw new Exception(_i18nService.T("UserAlreadyInvited", email, organization.DisplayName()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accepted or Confirmed - create SSO link and return;
|
// Accepted or Confirmed - create SSO link and return;
|
||||||
@ -516,7 +517,7 @@ public class AccountController : Controller
|
|||||||
await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value, prorationDate);
|
await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value, prorationDate);
|
||||||
}
|
}
|
||||||
_logger.LogInformation(e, "SSO auto provisioning failed");
|
_logger.LogInformation(e, "SSO auto provisioning failed");
|
||||||
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.Name));
|
throw new Exception(_i18nService.T("NoSeatsAvailable", organization.DisplayName()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -458,17 +458,112 @@ public class ProviderServiceTests
|
|||||||
{
|
{
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
|
||||||
|
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||||
|
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
|
||||||
|
|
||||||
|
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||||
|
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||||
|
|
||||||
|
await providerOrganizationRepository.Received(1)
|
||||||
|
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||||
|
providerOrganization.ProviderId == provider.Id &&
|
||||||
|
providerOrganization.OrganizationId == organization.Id &&
|
||||||
|
providerOrganization.Key == key));
|
||||||
|
|
||||||
|
await organizationRepository.Received(1)
|
||||||
|
.ReplaceAsync(Arg.Is<Organization>(org => org.BillingEmail == provider.BillingEmail));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerUpdateAsync(
|
||||||
|
organization.GatewayCustomerId,
|
||||||
|
Arg.Is<CustomerUpdateOptions>(options => options.Email == provider.BillingEmail));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||||
|
providerOrganization.ProviderId == provider.Id &&
|
||||||
|
providerOrganization.OrganizationId == organization.Id &&
|
||||||
|
providerOrganization.Key == key),
|
||||||
|
EventType.ProviderOrganization_Added);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
|
||||||
|
SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
var expectedPlanType = PlanType.EnterpriseAnnually;
|
||||||
|
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||||
|
|
||||||
|
await providerOrganizationRepository.Received(1)
|
||||||
|
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||||
|
providerOrganization.ProviderId == provider.Id &&
|
||||||
|
providerOrganization.OrganizationId == organization.Id &&
|
||||||
|
providerOrganization.Key == key));
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||||
|
providerOrganization.ProviderId == provider.Id &&
|
||||||
|
providerOrganization.OrganizationId == organization.Id &&
|
||||||
|
providerOrganization.Key == key),
|
||||||
|
EventType.ProviderOrganization_Added);
|
||||||
|
|
||||||
|
Assert.Equal(organization.PlanType, expectedPlanType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,
|
||||||
|
SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
var newCreationDate = new DateTime(2023, 11, 5);
|
||||||
|
BackdateProviderCreationDate(provider, newCreationDate);
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
|
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
organization.Plan = "Enterprise (Annually)";
|
||||||
|
|
||||||
|
var expectedPlanType = PlanType.EnterpriseAnnually2020;
|
||||||
|
|
||||||
|
var expectedPlanId = "2020-enterprise-org-seat-annually";
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
|
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
||||||
|
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
||||||
|
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
|
||||||
|
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
|
||||||
|
|
||||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||||
|
|
||||||
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
|
await providerOrganizationRepository.Received(1)
|
||||||
|
.CreateAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||||
|
providerOrganization.ProviderId == provider.Id &&
|
||||||
|
providerOrganization.OrganizationId == organization.Id &&
|
||||||
|
providerOrganization.Key == key));
|
||||||
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
await sutProvider.GetDependency<IEventService>()
|
||||||
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
|
.Received().LogProviderOrganizationEventAsync(Arg.Is<ProviderOrganization>(providerOrganization =>
|
||||||
|
providerOrganization.ProviderId == provider.Id &&
|
||||||
|
providerOrganization.OrganizationId == organization.Id &&
|
||||||
|
providerOrganization.Key == key),
|
||||||
EventType.ProviderOrganization_Added);
|
EventType.ProviderOrganization_Added);
|
||||||
|
|
||||||
|
Assert.Equal(organization.PlanType, expectedPlanType);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@ -576,65 +671,6 @@ public class ProviderServiceTests
|
|||||||
t.First().Item2 == null));
|
t.First().Item2 == null));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task AddOrganization_CreateAfterNov62023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
|
|
||||||
SutProvider<ProviderService> sutProvider)
|
|
||||||
{
|
|
||||||
provider.Type = ProviderType.Msp;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
|
||||||
|
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
|
||||||
var expectedPlanType = PlanType.EnterpriseAnnually;
|
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
|
|
||||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
|
||||||
|
|
||||||
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
|
|
||||||
EventType.ProviderOrganization_Added);
|
|
||||||
Assert.Equal(organization.PlanType, expectedPlanType);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task AddOrganization_CreateBeforeNov62023_PlanTypeUpdated(Provider provider, Organization organization, string key,
|
|
||||||
SutProvider<ProviderService> sutProvider)
|
|
||||||
{
|
|
||||||
var newCreationDate = new DateTime(2023, 11, 5);
|
|
||||||
BackdateProviderCreationDate(provider, newCreationDate);
|
|
||||||
provider.Type = ProviderType.Msp;
|
|
||||||
|
|
||||||
organization.PlanType = PlanType.EnterpriseAnnually;
|
|
||||||
organization.Plan = "Enterprise (Annually)";
|
|
||||||
|
|
||||||
var expectedPlanType = PlanType.EnterpriseAnnually2020;
|
|
||||||
|
|
||||||
var expectedPlanId = "2020-enterprise-org-seat-annually";
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
|
||||||
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
|
||||||
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
|
||||||
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
|
||||||
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
|
|
||||||
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
|
|
||||||
|
|
||||||
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
|
||||||
|
|
||||||
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
|
|
||||||
EventType.ProviderOrganization_Added);
|
|
||||||
|
|
||||||
Assert.Equal(organization.PlanType, expectedPlanType);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
|
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
#
|
||||||
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
|
# !!! UPDATED 2024 for MsSqlMigratorUtility !!!
|
||||||
|
#
|
||||||
|
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
|
||||||
# where it takes ~40ms to connect to the terminal output of the container, so stuff logged to the terminal in this time is lost.
|
# where it takes ~40ms to connect to the terminal output of the container, so stuff logged to the terminal in this time is lost.
|
||||||
# The best workaround seems to be adding tiny delay like so:
|
# The best workaround seems to be adding tiny delay like so:
|
||||||
sleep 0.1;
|
sleep 0.1;
|
||||||
|
|
||||||
MIGRATE_DIRECTORY="/mnt/migrator/DbScripts"
|
|
||||||
LAST_MIGRATION_FILE="/mnt/data/last_migration"
|
|
||||||
SERVER='mssql'
|
SERVER='mssql'
|
||||||
DATABASE="vault_dev"
|
DATABASE="vault_dev"
|
||||||
USER="SA"
|
USER="SA"
|
||||||
@ -16,58 +16,33 @@ while getopts "s" arg; do
|
|||||||
case $arg in
|
case $arg in
|
||||||
s)
|
s)
|
||||||
echo "Running for self-host environment"
|
echo "Running for self-host environment"
|
||||||
LAST_MIGRATION_FILE="/mnt/data/last_self_host_migration"
|
|
||||||
DATABASE="vault_dev_self_host"
|
DATABASE="vault_dev_self_host"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ ! -f "$LAST_MIGRATION_FILE" ]; then
|
QUERY="IF OBJECT_ID('[$DATABASE].[dbo].[Migration]') IS NULL AND OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NOT NULL
|
||||||
echo "No migration file, nothing to migrate to a database store"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
LAST_MIGRATION=$(cat $LAST_MIGRATION_FILE)
|
|
||||||
rm $LAST_MIGRATION_FILE
|
|
||||||
fi
|
|
||||||
|
|
||||||
[ -z "$LAST_MIGRATION" ]
|
|
||||||
PERFORM_MIGRATION=$?
|
|
||||||
|
|
||||||
# Create database if it does not already exist
|
|
||||||
QUERY="IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'migrations_$DATABASE')
|
|
||||||
BEGIN
|
BEGIN
|
||||||
CREATE DATABASE migrations_$DATABASE;
|
-- Create [database].dbo.Migration with the schema expected by MsSqlMigratorUtility
|
||||||
END;
|
SET ANSI_NULLS ON;
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
|
||||||
|
CREATE TABLE [$DATABASE].[dbo].[Migration](
|
||||||
|
[Id] [int] IDENTITY(1,1) NOT NULL,
|
||||||
|
[ScriptName] [nvarchar](255) NOT NULL,
|
||||||
|
[Applied] [datetime] NOT NULL
|
||||||
|
) ON [PRIMARY];
|
||||||
|
|
||||||
|
ALTER TABLE [$DATABASE].[dbo].[Migration] ADD CONSTRAINT [PK_Migration_Id] PRIMARY KEY CLUSTERED
|
||||||
|
(
|
||||||
|
[Id] ASC
|
||||||
|
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY];
|
||||||
|
|
||||||
|
-- Copy across old data
|
||||||
|
INSERT INTO [$DATABASE].[dbo].[Migration] (ScriptName, Applied)
|
||||||
|
SELECT CONCAT('Bit.Migrator.DbScripts.', [Filename]), CreationDate
|
||||||
|
FROM [migrations_$DATABASE].[dbo].[migrations];
|
||||||
|
END
|
||||||
"
|
"
|
||||||
|
|
||||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
|
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
|
||||||
|
|
||||||
QUERY="IF OBJECT_ID('[dbo].[migrations_$DATABASE]') IS NULL
|
|
||||||
BEGIN
|
|
||||||
CREATE TABLE [migrations_$DATABASE].[dbo].[migrations] (
|
|
||||||
[Id] INT IDENTITY(1,1) PRIMARY KEY,
|
|
||||||
[Filename] NVARCHAR(MAX) NOT NULL,
|
|
||||||
[CreationDate] DATETIME2 (7) NULL,
|
|
||||||
);
|
|
||||||
END;"
|
|
||||||
|
|
||||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
|
|
||||||
|
|
||||||
record_migration () {
|
|
||||||
echo "recording $1"
|
|
||||||
local file=$(basename $1)
|
|
||||||
echo $file
|
|
||||||
local query="INSERT INTO [migrations] ([Filename], [CreationDate]) VALUES ('$file', GETUTCDATE())"
|
|
||||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query"
|
|
||||||
}
|
|
||||||
|
|
||||||
for f in `ls -v $MIGRATE_DIRECTORY/*.sql`; do
|
|
||||||
if (( PERFORM_MIGRATION == 0 )); then
|
|
||||||
echo "Still need to migrate $f"
|
|
||||||
else
|
|
||||||
record_migration $f
|
|
||||||
if [ "$LAST_MIGRATION" == "$f" ]; then
|
|
||||||
PERFORM_MIGRATION=0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done;
|
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# There seems to be [a bug with docker-compose](https://github.com/docker/compose/issues/4076#issuecomment-324932294)
|
|
||||||
# where it takes ~40ms to connect to the terminal output of the container, so stuff logged to the terminal in this time is lost.
|
|
||||||
# The best workaround seems to be adding tiny delay like so:
|
|
||||||
sleep 0.1;
|
|
||||||
|
|
||||||
MIGRATE_DIRECTORY="/mnt/migrator/DbScripts"
|
|
||||||
SERVER='mssql'
|
|
||||||
DATABASE="vault_dev"
|
|
||||||
USER="SA"
|
|
||||||
PASSWD=$MSSQL_PASSWORD
|
|
||||||
|
|
||||||
while getopts "sp" arg; do
|
|
||||||
case $arg in
|
|
||||||
s)
|
|
||||||
echo "Running for self-host environment"
|
|
||||||
DATABASE="vault_dev_self_host"
|
|
||||||
;;
|
|
||||||
p)
|
|
||||||
echo "Running for pipeline"
|
|
||||||
MIGRATE_DIRECTORY=$MSSQL_MIGRATIONS_DIRECTORY
|
|
||||||
SERVER=$MSSQL_HOST
|
|
||||||
DATABASE=$MSSQL_DATABASE
|
|
||||||
USER=$MSSQL_USER
|
|
||||||
PASSWD=$MSSQL_PASS
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# Create databases if they do not already exist
|
|
||||||
QUERY="IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = '$DATABASE')
|
|
||||||
BEGIN
|
|
||||||
CREATE DATABASE $DATABASE;
|
|
||||||
END;
|
|
||||||
|
|
||||||
GO
|
|
||||||
IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'migrations_$DATABASE')
|
|
||||||
BEGIN
|
|
||||||
CREATE DATABASE migrations_$DATABASE;
|
|
||||||
END;
|
|
||||||
|
|
||||||
GO
|
|
||||||
"
|
|
||||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d master -U $USER -P $PASSWD -I -Q "$QUERY"
|
|
||||||
echo "Return code: $?"
|
|
||||||
|
|
||||||
# Create migrations table if it does not already exist
|
|
||||||
QUERY="IF OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NULL
|
|
||||||
BEGIN
|
|
||||||
CREATE TABLE [migrations_$DATABASE].[dbo].[migrations] (
|
|
||||||
[Id] INT IDENTITY(1,1) PRIMARY KEY,
|
|
||||||
[Filename] NVARCHAR(MAX) NOT NULL,
|
|
||||||
[CreationDate] DATETIME2 (7) NULL,
|
|
||||||
);
|
|
||||||
END;
|
|
||||||
GO
|
|
||||||
"
|
|
||||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$QUERY"
|
|
||||||
echo "Return code: $?"
|
|
||||||
|
|
||||||
should_migrate () {
|
|
||||||
local file=$(basename $1)
|
|
||||||
local query="SELECT * FROM [migrations] WHERE [Filename] = '$file'"
|
|
||||||
local result=$(/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query")
|
|
||||||
if [[ "$result" =~ .*"$file".* ]]; then
|
|
||||||
return 1;
|
|
||||||
else
|
|
||||||
return 0;
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
record_migration () {
|
|
||||||
echo "recording $1"
|
|
||||||
local file=$(basename $1)
|
|
||||||
echo $file
|
|
||||||
local query="INSERT INTO [migrations] ([Filename], [CreationDate]) VALUES ('$file', GETUTCDATE())"
|
|
||||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d migrations_$DATABASE -U $USER -P $PASSWD -I -Q "$query"
|
|
||||||
}
|
|
||||||
|
|
||||||
migrate () {
|
|
||||||
local file=$1
|
|
||||||
echo "Performing $file"
|
|
||||||
/opt/mssql-tools/bin/sqlcmd -S $SERVER -d $DATABASE -U $USER -P $PASSWD -I -i $file
|
|
||||||
}
|
|
||||||
|
|
||||||
for f in `ls -v $MIGRATE_DIRECTORY/*.sql`; do
|
|
||||||
BASENAME=$(basename $f)
|
|
||||||
if should_migrate $f == 1 ; then
|
|
||||||
migrate $f
|
|
||||||
record_migration $f
|
|
||||||
else
|
|
||||||
echo "Skipping $f, $BASENAME"
|
|
||||||
fi
|
|
||||||
done;
|
|
@ -2,20 +2,20 @@
|
|||||||
# Creates the vault_dev database, and runs all the migrations.
|
# Creates the vault_dev database, and runs all the migrations.
|
||||||
|
|
||||||
# Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use
|
# Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use
|
||||||
# the mssql-tools container which runs under x86_64. We should monitor this
|
# the mssql-tools container which runs under x86_64.
|
||||||
# in the future and investigate if we can migrate back.
|
|
||||||
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
|
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[switch]$all = $false,
|
[switch]$all,
|
||||||
[switch]$postgres = $false,
|
[switch]$postgres,
|
||||||
[switch]$mysql = $false,
|
[switch]$mysql,
|
||||||
[switch]$mssql = $false,
|
[switch]$mssql,
|
||||||
[switch]$sqlite = $false,
|
[switch]$sqlite,
|
||||||
[switch]$selfhost = $false,
|
[switch]$selfhost
|
||||||
[switch]$pipeline = $false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Abort on any error
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
|
if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
|
||||||
$mssql = $true;
|
$mssql = $true;
|
||||||
}
|
}
|
||||||
@ -29,22 +29,27 @@ if ($all -or $postgres -or $mysql -or $sqlite) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($all -or $mssql) {
|
if ($all -or $mssql) {
|
||||||
if ($selfhost) {
|
function Get-UserSecrets {
|
||||||
$migrationArgs = "-s"
|
return dotnet user-secrets list --json --project ../src/Api | ConvertFrom-Json
|
||||||
} elseif ($pipeline) {
|
|
||||||
$migrationArgs = "-p"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Starting Microsoft SQL Server Migrations"
|
if ($selfhost) {
|
||||||
docker run `
|
$msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
|
||||||
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
|
$envName = "self-host"
|
||||||
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
|
|
||||||
-v "$(pwd)/.data/mssql:/mnt/data" `
|
Write-Output "Migrating your migrations to use MsSqlMigratorUtility (if needed)"
|
||||||
--env-file .env `
|
./migrate_migration_record.ps1 -s
|
||||||
--network=bitwardenserver_default `
|
} else {
|
||||||
--rm `
|
$msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
|
||||||
mcr.microsoft.com/mssql-tools `
|
$envName = "cloud"
|
||||||
/mnt/helpers/run_migrations.sh $migrationArgs
|
|
||||||
|
Write-Output "Migrating your migrations to use MsSqlMigratorUtility (if needed)"
|
||||||
|
./migrate_migration_record.ps1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Starting Microsoft SQL Server Migrations for $envName"
|
||||||
|
|
||||||
|
dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString"
|
||||||
}
|
}
|
||||||
|
|
||||||
$currentDir = Get-Location
|
$currentDir = Get-Location
|
||||||
|
@ -1,15 +1,13 @@
|
|||||||
#!/usr/bin/env pwsh
|
#!/usr/bin/env pwsh
|
||||||
# This script need only be run once
|
# !!! UPDATED 2024 for MsSqlMigratorUtility !!!
|
||||||
#
|
#
|
||||||
# This is a migration script for updating recording the last migration run
|
# This is a migration script to move data from [migrations_vault_dev].[dbo].[migrations] (used by our custom
|
||||||
# in a file to recording migrations in a database table. It will create a
|
# migrator script) to [vault_dev].[dbo].[Migration] (used by MsSqlMigratorUtility). It is safe to run multiple
|
||||||
# migrations_vault table and store all of the previously run migrations as
|
# times because it will not perform any migration if it detects that the new table is already present.
|
||||||
# indicated by a last_migrations file. It will then delete this file.
|
# This will be deleted after a few months after everyone has (presumably) migrated to the new schema.
|
||||||
|
|
||||||
# Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use
|
# Due to azure-edge-sql not containing the mssql-tools on ARM, we manually use
|
||||||
# the mssql-tools container which runs under x86_64. We should monitor this
|
# the mssql-tools container which runs under x86_64.
|
||||||
# in the future and investigate if we can migrate back.
|
|
||||||
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
|
|
||||||
|
|
||||||
docker run `
|
docker run `
|
||||||
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
|
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Admin.AdminConsole.Models;
|
using System.Net;
|
||||||
|
using Bit.Admin.AdminConsole.Models;
|
||||||
using Bit.Admin.Enums;
|
using Bit.Admin.Enums;
|
||||||
using Bit.Admin.Services;
|
using Bit.Admin.Services;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
@ -119,8 +120,9 @@ public class OrganizationsController : Controller
|
|||||||
count = 1;
|
count = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var encodedName = WebUtility.HtmlEncode(name);
|
||||||
var skip = (page - 1) * count;
|
var skip = (page - 1) * count;
|
||||||
var organizations = await _organizationRepository.SearchAsync(name, userEmail, paid, skip, count);
|
var organizations = await _organizationRepository.SearchAsync(encodedName, userEmail, paid, skip, count);
|
||||||
return View(new OrganizationsModel
|
return View(new OrganizationsModel
|
||||||
{
|
{
|
||||||
Items = organizations as List<Organization>,
|
Items = organizations as List<Organization>,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Admin.AdminConsole.Models;
|
using System.Net;
|
||||||
|
using Bit.Admin.AdminConsole.Models;
|
||||||
using Bit.Admin.Enums;
|
using Bit.Admin.Enums;
|
||||||
using Bit.Admin.Utilities;
|
using Bit.Admin.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
@ -188,8 +189,9 @@ public class ProvidersController : Controller
|
|||||||
count = 1;
|
count = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var encodedName = WebUtility.HtmlEncode(name);
|
||||||
var skip = (page - 1) * count;
|
var skip = (page - 1) * count;
|
||||||
var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(name, ownerEmail, skip, count);
|
var unassignedOrganizations = await _organizationRepository.SearchUnassignedToProviderAsync(encodedName, ownerEmail, skip, count);
|
||||||
var viewModel = new OrganizationUnassignedToProviderSearchViewModel
|
var viewModel = new OrganizationUnassignedToProviderSearchViewModel
|
||||||
{
|
{
|
||||||
OrganizationName = string.IsNullOrWhiteSpace(name) ? null : name,
|
OrganizationName = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||||
@ -199,7 +201,7 @@ public class ProvidersController : Controller
|
|||||||
Items = unassignedOrganizations.Select(uo => new OrganizationSelectableViewModel
|
Items = unassignedOrganizations.Select(uo => new OrganizationSelectableViewModel
|
||||||
{
|
{
|
||||||
Id = uo.Id,
|
Id = uo.Id,
|
||||||
Name = uo.Name,
|
Name = uo.DisplayName(),
|
||||||
PlanType = uo.PlanType
|
PlanType = uo.PlanType
|
||||||
}).ToList()
|
}).ToList()
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Net;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
@ -36,8 +37,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
BillingInfo = billingInfo;
|
BillingInfo = billingInfo;
|
||||||
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
|
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||||
|
|
||||||
Name = org.Name;
|
Name = org.DisplayName();
|
||||||
BusinessName = org.BusinessName;
|
BusinessName = org.DisplayBusinessName();
|
||||||
BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;
|
BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;
|
||||||
PlanType = org.PlanType;
|
PlanType = org.PlanType;
|
||||||
Plan = org.Plan;
|
Plan = org.Plan;
|
||||||
@ -184,8 +185,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
|
|
||||||
public Organization ToOrganization(Organization existingOrganization)
|
public Organization ToOrganization(Organization existingOrganization)
|
||||||
{
|
{
|
||||||
existingOrganization.Name = Name;
|
existingOrganization.Name = WebUtility.HtmlEncode(Name.Trim());
|
||||||
existingOrganization.BusinessName = BusinessName;
|
existingOrganization.BusinessName = WebUtility.HtmlEncode(BusinessName.Trim());
|
||||||
existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||||
existingOrganization.PlanType = PlanType.Value;
|
existingOrganization.PlanType = PlanType.Value;
|
||||||
existingOrganization.Plan = Plan;
|
existingOrganization.Plan = Plan;
|
||||||
|
@ -11,8 +11,8 @@ public class ProviderEditModel : ProviderViewModel
|
|||||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
||||||
: base(provider, providerUsers, organizations)
|
: base(provider, providerUsers, organizations)
|
||||||
{
|
{
|
||||||
Name = provider.Name;
|
Name = provider.DisplayName();
|
||||||
BusinessName = provider.BusinessName;
|
BusinessName = provider.DisplayBusinessName();
|
||||||
BillingEmail = provider.BillingEmail;
|
BillingEmail = provider.BillingEmail;
|
||||||
BillingPhone = provider.BillingPhone;
|
BillingPhone = provider.BillingPhone;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||||
@model OrganizationEditModel
|
@model OrganizationEditModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Organization.Name;
|
ViewData["Title"] = (Model.Provider != null ? "Client " : string.Empty) + "Organization: " + Model.Name;
|
||||||
|
|
||||||
var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View);
|
var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View);
|
||||||
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
|
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
|
||||||
@ -58,7 +58,7 @@
|
|||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
|
|
||||||
<h1>@(Model.Provider != null ? "Client " : string.Empty)Organization <small>@Model.Organization.Name</small></h1>
|
<h1>@(Model.Provider != null ? "Client " : string.Empty)Organization <small>@Model.Name</small></h1>
|
||||||
|
|
||||||
@if (Model.Provider != null)
|
@if (Model.Provider != null)
|
||||||
{
|
{
|
||||||
|
@ -46,7 +46,7 @@
|
|||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a asp-action="@Model.Action" asp-route-id="@org.Id">@org.Name</a>
|
<a asp-action="@Model.Action" asp-route-id="@org.Id">@org.DisplayName()</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@org.Plan
|
@org.Plan
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
|
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
|
||||||
@model OrganizationViewModel
|
@model OrganizationViewModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Organization: " + Model.Organization.Name;
|
ViewData["Title"] = "Organization: " + Model.Organization.DisplayName();
|
||||||
}
|
}
|
||||||
|
|
||||||
<h1>Organization <small>@Model.Organization.Name</small></h1>
|
<h1>Organization <small>@Model.Organization.DisplayName()</small></h1>
|
||||||
|
|
||||||
@if (Model.Provider != null)
|
@if (Model.Provider != null)
|
||||||
{
|
{
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
@model Bit.Core.AdminConsole.Entities.Provider.Provider
|
@model Bit.Core.AdminConsole.Entities.Provider.Provider
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-4 col-lg-3">Provider Name</dt>
|
<dt class="col-sm-4 col-lg-3">Provider Name</dt>
|
||||||
<dd class="col-sm-8 col-lg-9">@Model.Name</dd>
|
<dd class="col-sm-8 col-lg-9">@Model.DisplayName()</dd>
|
||||||
|
|
||||||
<dt class="col-sm-4 col-lg-3">Provider Type</dt>
|
<dt class="col-sm-4 col-lg-3">Provider Type</dt>
|
||||||
<dd class="col-sm-8 col-lg-9">@(Model.Type.GetDisplayAttribute()?.GetName())</dd>
|
<dd class="col-sm-8 col-lg-9">@(Model.Type.GetDisplayAttribute()?.GetName())</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
@Html.HiddenFor(m => Model.Items[i].Id, new { @readonly = "readonly", autocomplete = "off" })
|
@Html.HiddenFor(m => Model.Items[i].Id, new { @readonly = "readonly", autocomplete = "off" })
|
||||||
@Html.CheckBoxFor(m => Model.Items[i].Selected)
|
@Html.CheckBoxFor(m => Model.Items[i].Selected)
|
||||||
</td>
|
</td>
|
||||||
<td>@Html.ActionLink(Model.Items[i].Name, "Edit", "Organizations", new { id = Model.Items[i].Id }, new { target = "_blank" })</td>
|
<td>@Html.ActionLink(Model.Items[i].DisplayName(), "Edit", "Organizations", new { id = Model.Items[i].Id }, new { target = "_blank" })</td>
|
||||||
<td>@(Model.Items[i].PlanType.GetDisplayAttribute()?.Name ?? Model.Items[i].PlanType.ToString())</td>
|
<td>@(Model.Items[i].PlanType.GetDisplayAttribute()?.Name ?? Model.Items[i].PlanType.ToString())</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,12 @@
|
|||||||
|
|
||||||
@model ProviderEditModel
|
@model ProviderEditModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
|
||||||
|
|
||||||
var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
var canEdit = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||||
}
|
}
|
||||||
|
|
||||||
<h1>Provider <small>@Model.Provider.Name</small></h1>
|
<h1>Provider <small>@Model.Provider.DisplayName()</small></h1>
|
||||||
|
|
||||||
<h2>Provider Information</h2>
|
<h2>Provider Information</h2>
|
||||||
@await Html.PartialAsync("_ViewInformation", Model)
|
@await Html.PartialAsync("_ViewInformation", Model)
|
||||||
@ -17,12 +17,12 @@
|
|||||||
<h2>General</h2>
|
<h2>General</h2>
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-4 col-lg-3">Name</dt>
|
<dt class="col-sm-4 col-lg-3">Name</dt>
|
||||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.Name</dd>
|
<dd class="col-sm-8 col-lg-9">@Model.Provider.DisplayName()</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<h2>Business Information</h2>
|
<h2>Business Information</h2>
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-4 col-lg-3">Business Name</dt>
|
<dt class="col-sm-4 col-lg-3">Business Name</dt>
|
||||||
<dd class="col-sm-8 col-lg-9">@Model.Provider.BusinessName</dd>
|
<dd class="col-sm-8 col-lg-9">@Model.Provider.DisplayBusinessName()</dd>
|
||||||
</dl>
|
</dl>
|
||||||
<h2>Billing</h2>
|
<h2>Billing</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(provider.Name ?? "Pending")</a>
|
<a asp-action="@Model.Action" asp-route-id="@provider.Id">@(!string.IsNullOrEmpty(provider.DisplayName()) ? provider.DisplayName() : "Pending")</a>
|
||||||
</td>
|
</td>
|
||||||
<td>@provider.Type.GetDisplayAttribute()?.GetShortName()</td>
|
<td>@provider.Type.GetDisplayAttribute()?.GetShortName()</td>
|
||||||
<td>@provider.Status</td>
|
<td>@provider.Status</td>
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.OrganizationName</a>
|
<a asp-controller="Organizations" asp-action="Edit" asp-route-id="@providerOrganization.OrganizationId">@providerOrganization.DisplayName()</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@providerOrganization.Status
|
@providerOrganization.Status
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
@model ProviderViewModel
|
@model ProviderViewModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
|
||||||
}
|
}
|
||||||
|
|
||||||
<h1>Provider <small>@Model.Provider.Name</small></h1>
|
<h1>Provider <small>@Model.Provider.DisplayName()</small></h1>
|
||||||
|
|
||||||
<h2>Information</h2>
|
<h2>Information</h2>
|
||||||
@await Html.PartialAsync("_ViewInformation", Model)
|
@await Html.PartialAsync("_ViewInformation", Model)
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Name"></label>
|
<label asp-for="Name"></label>
|
||||||
<input type="text" class="form-control" asp-for="Name" required>
|
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -68,7 +68,7 @@
|
|||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="BusinessName"></label>
|
<label asp-for="BusinessName"></label>
|
||||||
<input type="text" class="form-control" asp-for="BusinessName">
|
<input type="text" class="form-control" asp-for="BusinessName" value="@Model.BusinessName">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,6 +87,7 @@ public static class RolePermissionMapping
|
|||||||
Permission.Provider_List_View,
|
Permission.Provider_List_View,
|
||||||
Permission.Provider_Create,
|
Permission.Provider_Create,
|
||||||
Permission.Provider_View,
|
Permission.Provider_View,
|
||||||
|
Permission.Provider_Edit,
|
||||||
Permission.Provider_ResendEmailInvite,
|
Permission.Provider_ResendEmailInvite,
|
||||||
Permission.Tools_ChargeBrainTreeCustomer,
|
Permission.Tools_ChargeBrainTreeCustomer,
|
||||||
Permission.Tools_PromoteAdmin,
|
Permission.Tools_PromoteAdmin,
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Api.Models.Request.Organizations;
|
|||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Utilities;
|
using Bit.Api.Utilities;
|
||||||
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
|
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
@ -41,6 +42,7 @@ public class OrganizationUsersController : Controller
|
|||||||
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public OrganizationUsersController(
|
public OrganizationUsersController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -56,7 +58,8 @@ public class OrganizationUsersController : Controller
|
|||||||
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
||||||
IAcceptOrgUserCommand acceptOrgUserCommand,
|
IAcceptOrgUserCommand acceptOrgUserCommand,
|
||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
IApplicationCacheService applicationCacheService)
|
IApplicationCacheService applicationCacheService,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -72,6 +75,7 @@ public class OrganizationUsersController : Controller
|
|||||||
_acceptOrgUserCommand = acceptOrgUserCommand;
|
_acceptOrgUserCommand = acceptOrgUserCommand;
|
||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@ -305,43 +309,34 @@ public class OrganizationUsersController : Controller
|
|||||||
|
|
||||||
[HttpPut("{id}")]
|
[HttpPut("{id}")]
|
||||||
[HttpPost("{id}")]
|
[HttpPost("{id}")]
|
||||||
public async Task Put(string orgId, string id, [FromBody] OrganizationUserUpdateRequestModel model)
|
public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
|
||||||
{
|
{
|
||||||
var orgGuidId = new Guid(orgId);
|
if (!await _currentContext.ManageUsers(orgId))
|
||||||
if (!await _currentContext.ManageUsers(orgGuidId))
|
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
|
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||||
if (organizationUser == null || organizationUser.OrganizationId != orgGuidId)
|
if (organizationUser == null || organizationUser.OrganizationId != orgId)
|
||||||
{
|
{
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User);
|
// If admins are not allowed access to all collections, you cannot add yourself to a group
|
||||||
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
|
// In this case we just don't update groups
|
||||||
model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups);
|
var userId = _userService.GetProperUserId(User).Value;
|
||||||
}
|
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
|
||||||
|
var restrictEditingGroups = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1) &&
|
||||||
|
organizationAbility.FlexibleCollections &&
|
||||||
|
userId == organizationUser.UserId &&
|
||||||
|
!organizationAbility.AllowAdminAccessToAllCollectionItems;
|
||||||
|
|
||||||
[HttpPut("{id}/groups")]
|
var groups = restrictEditingGroups
|
||||||
[HttpPost("{id}/groups")]
|
? null
|
||||||
public async Task PutGroups(string orgId, string id, [FromBody] OrganizationUserUpdateGroupsRequestModel model)
|
: model.Groups;
|
||||||
{
|
|
||||||
var orgGuidId = new Guid(orgId);
|
|
||||||
if (!await _currentContext.ManageUsers(orgGuidId))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
|
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId,
|
||||||
if (organizationUser == null || organizationUser.OrganizationId != orgGuidId)
|
model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), groups);
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var loggedInUserId = _userService.GetProperUserId(User);
|
|
||||||
await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(organizationUser, model.GroupIds.Select(g => new Guid(g)), loggedInUserId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{userId}/reset-password-enrollment")]
|
[HttpPut("{userId}/reset-password-enrollment")]
|
||||||
|
@ -261,19 +261,19 @@ public class OrganizationsController : Controller
|
|||||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}/risks-subscription-failure")]
|
[HttpGet("{id}/billing-status")]
|
||||||
public async Task<OrganizationRisksSubscriptionFailureResponseModel> RisksSubscriptionFailure(Guid id)
|
public async Task<OrganizationBillingStatusResponseModel> GetBillingStatus(Guid id)
|
||||||
{
|
{
|
||||||
if (!await _currentContext.EditPaymentMethods(id))
|
if (!await _currentContext.EditPaymentMethods(id))
|
||||||
{
|
{
|
||||||
return new OrganizationRisksSubscriptionFailureResponseModel(id, false);
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||||
|
|
||||||
var risksSubscriptionFailure = await _paymentService.RisksSubscriptionFailure(organization);
|
var risksSubscriptionFailure = await _paymentService.RisksSubscriptionFailure(organization);
|
||||||
|
|
||||||
return new OrganizationRisksSubscriptionFailureResponseModel(id, risksSubscriptionFailure);
|
return new OrganizationBillingStatusResponseModel(organization, risksSubscriptionFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("")]
|
[HttpPost("")]
|
||||||
@ -303,7 +303,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.BusinessName ||
|
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.DisplayBusinessName() ||
|
||||||
model.BillingEmail != organization.BillingEmail);
|
model.BillingEmail != organization.BillingEmail);
|
||||||
|
|
||||||
var hasRequiredPermissions = updateBilling
|
var hasRequiredPermissions = updateBilling
|
||||||
@ -464,8 +464,8 @@ public class OrganizationsController : Controller
|
|||||||
await _organizationService.VerifyBankAsync(orgIdGuid, model.Amount1.Value, model.Amount2.Value);
|
await _organizationService.VerifyBankAsync(orgIdGuid, model.Amount1.Value, model.Amount2.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/churn")]
|
[HttpPost("{id}/cancel")]
|
||||||
public async Task PostChurn(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
|
public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
|
||||||
{
|
{
|
||||||
if (!await _currentContext.EditSubscription(id))
|
if (!await _currentContext.EditSubscription(id))
|
||||||
{
|
{
|
||||||
@ -499,19 +499,6 @@ public class OrganizationsController : Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/cancel")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PostCancel(string id)
|
|
||||||
{
|
|
||||||
var orgIdGuid = new Guid(id);
|
|
||||||
if (!await _currentContext.EditSubscription(orgIdGuid))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _organizationService.CancelSubscriptionAsync(orgIdGuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("{id}/reinstate")]
|
[HttpPost("{id}/reinstate")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task PostReinstate(string id)
|
public async Task PostReinstate(string id)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
@ -9,9 +10,11 @@ namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
|||||||
public class OrganizationCreateRequestModel : IValidatableObject
|
public class OrganizationCreateRequestModel : IValidatableObject
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(50)]
|
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
[StringLength(50)]
|
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string BusinessName { get; set; }
|
public string BusinessName { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||||
|
|
||||||
public class OrganizationUpdateRequestModel
|
public class OrganizationUpdateRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(50)]
|
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
[StringLength(50)]
|
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string BusinessName { get; set; }
|
public string BusinessName { get; set; }
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
[Required]
|
[Required]
|
||||||
|
@ -102,12 +102,6 @@ public class OrganizationUserUpdateRequestModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OrganizationUserUpdateGroupsRequestModel
|
|
||||||
{
|
|
||||||
[Required]
|
|
||||||
public IEnumerable<string> GroupIds { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OrganizationUserResetPasswordEnrollmentRequestModel
|
public class OrganizationUserResetPasswordEnrollmentRequestModel
|
||||||
{
|
{
|
||||||
public string ResetPasswordKey { get; set; }
|
public string ResetPasswordKey { get; set; }
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Models.Request.Providers;
|
namespace Bit.Api.AdminConsole.Models.Request.Providers;
|
||||||
|
|
||||||
public class ProviderSetupRequestModel
|
public class ProviderSetupRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(50)]
|
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
[StringLength(50)]
|
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string BusinessName { get; set; }
|
public string BusinessName { get; set; }
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(256)]
|
[StringLength(256)]
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Models.Request.Providers;
|
namespace Bit.Api.AdminConsole.Models.Request.Providers;
|
||||||
|
|
||||||
public class ProviderUpdateRequestModel
|
public class ProviderUpdateRequestModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[StringLength(50)]
|
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
[StringLength(50)]
|
[StringLength(50, ErrorMessage = "The field Business Name exceeds the maximum length.")]
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string BusinessName { get; set; }
|
public string BusinessName { get; set; }
|
||||||
[EmailAddress]
|
[EmailAddress]
|
||||||
[Required]
|
[Required]
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Models.Api;
|
||||||
|
|
||||||
|
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||||
|
|
||||||
|
public class OrganizationBillingStatusResponseModel(
|
||||||
|
Organization organization,
|
||||||
|
bool risksSubscriptionFailure) : ResponseModel("organizationBillingStatus")
|
||||||
|
{
|
||||||
|
public Guid OrganizationId { get; } = organization.Id;
|
||||||
|
public string OrganizationName { get; } = organization.Name;
|
||||||
|
public bool RisksSubscriptionFailure { get; } = risksSubscriptionFailure;
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Api.Models.Response;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
@ -60,7 +61,9 @@ public class OrganizationResponseModel : ResponseModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string BusinessName { get; set; }
|
public string BusinessName { get; set; }
|
||||||
public string BusinessAddress1 { get; set; }
|
public string BusinessAddress1 { get; set; }
|
||||||
public string BusinessAddress2 { get; set; }
|
public string BusinessAddress2 { get; set; }
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
using Bit.Core.Models.Api;
|
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
|
||||||
|
|
||||||
public class OrganizationRisksSubscriptionFailureResponseModel : ResponseModel
|
|
||||||
{
|
|
||||||
public Guid OrganizationId { get; }
|
|
||||||
public bool RisksSubscriptionFailure { get; }
|
|
||||||
|
|
||||||
public OrganizationRisksSubscriptionFailureResponseModel(
|
|
||||||
Guid organizationId,
|
|
||||||
bool risksSubscriptionFailure) : base("organizationRisksSubscriptionFailure")
|
|
||||||
{
|
|
||||||
OrganizationId = organizationId;
|
|
||||||
RisksSubscriptionFailure = risksSubscriptionFailure;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models.Data;
|
using Bit.Core.Auth.Models.Data;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -103,6 +104,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
public bool UseSso { get; set; }
|
public bool UseSso { get; set; }
|
||||||
@ -135,6 +137,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
public Guid? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
public bool HasPublicAndPrivateKeys { get; set; }
|
public bool HasPublicAndPrivateKeys { get; set; }
|
||||||
public Guid? ProviderId { get; set; }
|
public Guid? ProviderId { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string ProviderName { get; set; }
|
public string ProviderName { get; set; }
|
||||||
public ProviderType? ProviderType { get; set; }
|
public ProviderType? ProviderType { get; set; }
|
||||||
public string FamilySponsorshipFriendlyName { get; set; }
|
public string FamilySponsorshipFriendlyName { get; set; }
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
@ -23,6 +24,7 @@ public class ProfileProviderResponseModel : ResponseModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
public ProviderUserStatusType Status { get; set; }
|
public ProviderUserStatusType Status { get; set; }
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Models.Response.Providers;
|
namespace Bit.Api.AdminConsole.Models.Response.Providers;
|
||||||
|
|
||||||
@ -68,5 +70,6 @@ public class ProviderOrganizationOrganizationDetailsResponseModel : ProviderOrga
|
|||||||
OrganizationName = providerOrganization.OrganizationName;
|
OrganizationName = providerOrganization.OrganizationName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string OrganizationName { get; set; }
|
public string OrganizationName { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Models.Response.Providers;
|
namespace Bit.Api.AdminConsole.Models.Response.Providers;
|
||||||
|
|
||||||
@ -25,6 +27,7 @@ public class ProviderResponseModel : ResponseModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string BusinessName { get; set; }
|
public string BusinessName { get; set; }
|
||||||
public string BusinessAddress1 { get; set; }
|
public string BusinessAddress1 { get; set; }
|
||||||
|
@ -32,15 +32,10 @@
|
|||||||
</Choose>
|
</Choose>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" />
|
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.0" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.AzureStorage" Version="6.1.2" />
|
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.0" />
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Network" Version="6.0.4" />
|
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Redis" Version="6.0.4" />
|
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.SendGrid" Version="6.0.2" />
|
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="6.0.2" />
|
|
||||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="6.0.3" />
|
|
||||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.10.0" />
|
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.10.0" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -821,8 +821,8 @@ public class AccountsController : Controller
|
|||||||
await _userService.UpdateLicenseAsync(user, license);
|
await _userService.UpdateLicenseAsync(user, license);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("churn-premium")]
|
[HttpPost("cancel")]
|
||||||
public async Task PostChurn([FromBody] SubscriptionCancellationRequestModel request)
|
public async Task PostCancel([FromBody] SubscriptionCancellationRequestModel request)
|
||||||
{
|
{
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||||
|
|
||||||
@ -851,19 +851,6 @@ public class AccountsController : Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("cancel-premium")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PostCancel()
|
|
||||||
{
|
|
||||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _userService.CancelPremiumAsync(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("reinstate-premium")]
|
[HttpPost("reinstate-premium")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task PostReinstate()
|
public async Task PostReinstate()
|
||||||
|
@ -132,7 +132,7 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (syncResponseData, offersToSend) = await _syncSponsorshipsCommand.SyncOrganization(sponsoringOrg, model.ToOrganizationSponsorshipSync().SponsorshipsBatch);
|
var (syncResponseData, offersToSend) = await _syncSponsorshipsCommand.SyncOrganization(sponsoringOrg, model.ToOrganizationSponsorshipSync().SponsorshipsBatch);
|
||||||
await _sendSponsorshipOfferCommand.BulkSendSponsorshipOfferAsync(sponsoringOrg.Name, offersToSend);
|
await _sendSponsorshipOfferCommand.BulkSendSponsorshipOfferAsync(sponsoringOrg.DisplayName(), offersToSend);
|
||||||
return new OrganizationSponsorshipSyncResponseModel(syncResponseData);
|
return new OrganizationSponsorshipSyncResponseModel(syncResponseData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ public class SelfHostedSponsorshipSyncJob : BaseJob
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, $"Sponsorship sync for organization {org.Name} Failed");
|
_logger.LogError(ex, "Sponsorship sync for organization {OrganizationName} Failed", org.DisplayName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,34 +92,6 @@ public static class ServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
builder.AddSqlServer(globalSettings.SqlServer.ConnectionString);
|
builder.AddSqlServer(globalSettings.SqlServer.ConnectionString);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.DistributedCache?.Redis?.ConnectionString))
|
|
||||||
{
|
|
||||||
builder.AddRedis(globalSettings.DistributedCache.Redis.ConnectionString);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.Storage.ConnectionString))
|
|
||||||
{
|
|
||||||
builder.AddAzureQueueStorage(globalSettings.Storage.ConnectionString, name: "storage_queue")
|
|
||||||
.AddAzureQueueStorage(globalSettings.Events.ConnectionString, name: "events_queue");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.Notifications.ConnectionString))
|
|
||||||
{
|
|
||||||
builder.AddAzureQueueStorage(globalSettings.Notifications.ConnectionString,
|
|
||||||
name: "notifications_queue");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString))
|
|
||||||
{
|
|
||||||
builder.AddAzureServiceBusTopic(_ => globalSettings.ServiceBus.ConnectionString,
|
|
||||||
_ => globalSettings.ServiceBus.ApplicationCacheTopicName, name: "service_bus");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.Mail.SendGridApiKey))
|
|
||||||
{
|
|
||||||
builder.AddSendGrid(globalSettings.Mail.SendGridApiKey);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ public class FreshsalesController : Controller
|
|||||||
|
|
||||||
foreach (var org in orgs)
|
foreach (var org in orgs)
|
||||||
{
|
{
|
||||||
noteItems.Add($"Org, {org.Name}: {_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}");
|
noteItems.Add($"Org, {org.DisplayName()}: {_globalSettings.BaseServiceUri.Admin}/organizations/edit/{org.Id}");
|
||||||
if (TryGetPlanName(org.PlanType, out var planName))
|
if (TryGetPlanName(org.PlanType, out var planName))
|
||||||
{
|
{
|
||||||
newTags.Add($"Org: {planName}");
|
newTags.Add($"Org: {planName}");
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Bit.Billing.Models;
|
using Bit.Billing.Models;
|
||||||
using Bit.Billing.Services;
|
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -20,7 +19,6 @@ public class PayPalController : Controller
|
|||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IPaymentService _paymentService;
|
private readonly IPaymentService _paymentService;
|
||||||
private readonly IPayPalIPNClient _payPalIPNClient;
|
|
||||||
private readonly ITransactionRepository _transactionRepository;
|
private readonly ITransactionRepository _transactionRepository;
|
||||||
private readonly IUserRepository _userRepository;
|
private readonly IUserRepository _userRepository;
|
||||||
|
|
||||||
@ -30,7 +28,6 @@ public class PayPalController : Controller
|
|||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
IPayPalIPNClient payPalIPNClient,
|
|
||||||
ITransactionRepository transactionRepository,
|
ITransactionRepository transactionRepository,
|
||||||
IUserRepository userRepository)
|
IUserRepository userRepository)
|
||||||
{
|
{
|
||||||
@ -39,7 +36,6 @@ public class PayPalController : Controller
|
|||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_paymentService = paymentService;
|
_paymentService = paymentService;
|
||||||
_payPalIPNClient = payPalIPNClient;
|
|
||||||
_transactionRepository = transactionRepository;
|
_transactionRepository = transactionRepository;
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
}
|
}
|
||||||
@ -91,14 +87,6 @@ public class PayPalController : Controller
|
|||||||
return BadRequest();
|
return BadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
var verified = await _payPalIPNClient.VerifyIPN(transactionModel.TransactionId, requestContent);
|
|
||||||
|
|
||||||
if (!verified)
|
|
||||||
{
|
|
||||||
_logger.LogError("PayPal IPN ({Id}): Verification failed", transactionModel.TransactionId);
|
|
||||||
return BadRequest();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transactionModel.TransactionType != "web_accept" &&
|
if (transactionModel.TransactionType != "web_accept" &&
|
||||||
transactionModel.TransactionType != "merch_pmt" &&
|
transactionModel.TransactionType != "merch_pmt" &&
|
||||||
transactionModel.PaymentStatus != "Refunded")
|
transactionModel.PaymentStatus != "Refunded")
|
||||||
@ -204,8 +192,8 @@ public class PayPalController : Controller
|
|||||||
|
|
||||||
if (parentTransaction == null)
|
if (parentTransaction == null)
|
||||||
{
|
{
|
||||||
_logger.LogError("PayPal IPN ({Id}): Could not find parent transaction", transactionModel.TransactionId);
|
_logger.LogWarning("PayPal IPN ({Id}): Could not find parent transaction", transactionModel.TransactionId);
|
||||||
return BadRequest();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
var refundAmount = Math.Abs(transactionModel.MerchantGross);
|
var refundAmount = Math.Abs(transactionModel.MerchantGross);
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Billing.Models;
|
|||||||
using Bit.Billing.Services;
|
using Bit.Billing.Services;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
@ -188,7 +189,7 @@ public class StripeController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var user = await _userService.GetUserByIdAsync(userId);
|
var user = await _userService.GetUserByIdAsync(userId);
|
||||||
if (user.Premium)
|
if (user?.Premium == true)
|
||||||
{
|
{
|
||||||
await _userService.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd);
|
await _userService.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd);
|
||||||
}
|
}
|
||||||
@ -250,21 +251,21 @@ public class StripeController : Controller
|
|||||||
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
||||||
if (pm5766AutomaticTaxIsEnabled)
|
if (pm5766AutomaticTaxIsEnabled)
|
||||||
{
|
{
|
||||||
var customer = await _stripeFacade.GetCustomer(subscription.CustomerId);
|
var customerGetOptions = new CustomerGetOptions();
|
||||||
|
customerGetOptions.AddExpand("tax");
|
||||||
|
var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions);
|
||||||
if (!subscription.AutomaticTax.Enabled &&
|
if (!subscription.AutomaticTax.Enabled &&
|
||||||
!string.IsNullOrEmpty(customer.Address?.PostalCode) &&
|
customer.Tax?.AutomaticTax == StripeCustomerAutomaticTaxStatus.Supported)
|
||||||
!string.IsNullOrEmpty(customer.Address?.Country))
|
|
||||||
{
|
{
|
||||||
subscription = await _stripeFacade.UpdateSubscription(subscription.Id,
|
subscription = await _stripeFacade.UpdateSubscription(subscription.Id,
|
||||||
new SubscriptionUpdateOptions
|
new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
DefaultTaxRates = new List<string>(),
|
DefaultTaxRates = [],
|
||||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var updatedSubscription = pm5766AutomaticTaxIsEnabled
|
var updatedSubscription = pm5766AutomaticTaxIsEnabled
|
||||||
? subscription
|
? subscription
|
||||||
: await VerifyCorrectTaxRateForCharge(invoice, subscription);
|
: await VerifyCorrectTaxRateForCharge(invoice, subscription);
|
||||||
@ -319,7 +320,7 @@ public class StripeController : Controller
|
|||||||
{
|
{
|
||||||
var user = await _userService.GetUserByIdAsync(userId.Value);
|
var user = await _userService.GetUserByIdAsync(userId.Value);
|
||||||
|
|
||||||
if (user.Premium)
|
if (user?.Premium == true)
|
||||||
{
|
{
|
||||||
await SendEmails(new List<string> { user.Email });
|
await SendEmails(new List<string> { user.Email });
|
||||||
}
|
}
|
||||||
@ -571,7 +572,7 @@ public class StripeController : Controller
|
|||||||
else if (parsedEvent.Type.Equals(HandledStripeWebhook.CustomerUpdated))
|
else if (parsedEvent.Type.Equals(HandledStripeWebhook.CustomerUpdated))
|
||||||
{
|
{
|
||||||
var customer =
|
var customer =
|
||||||
await _stripeEventService.GetCustomer(parsedEvent, true, new List<string> { "subscriptions" });
|
await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]);
|
||||||
|
|
||||||
if (customer.Subscriptions == null || !customer.Subscriptions.Any())
|
if (customer.Subscriptions == null || !customer.Subscriptions.Any())
|
||||||
{
|
{
|
||||||
@ -614,7 +615,7 @@ public class StripeController : Controller
|
|||||||
{
|
{
|
||||||
Customer = paymentMethod.CustomerId,
|
Customer = paymentMethod.CustomerId,
|
||||||
Status = StripeSubscriptionStatus.Unpaid,
|
Status = StripeSubscriptionStatus.Unpaid,
|
||||||
Expand = new List<string> { "data.latest_invoice" }
|
Expand = ["data.latest_invoice"]
|
||||||
};
|
};
|
||||||
|
|
||||||
StripeList<Subscription> unpaidSubscriptions;
|
StripeList<Subscription> unpaidSubscriptions;
|
||||||
@ -672,9 +673,9 @@ public class StripeController : Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tuple<Guid?, Guid?> GetIdsFromMetaData(IDictionary<string, string> metaData)
|
private static Tuple<Guid?, Guid?> GetIdsFromMetaData(Dictionary<string, string> metaData)
|
||||||
{
|
{
|
||||||
if (metaData == null || !metaData.Any())
|
if (metaData == null || metaData.Count == 0)
|
||||||
{
|
{
|
||||||
return new Tuple<Guid?, Guid?>(null, null);
|
return new Tuple<Guid?, Guid?>(null, null);
|
||||||
}
|
}
|
||||||
@ -682,29 +683,35 @@ public class StripeController : Controller
|
|||||||
Guid? orgId = null;
|
Guid? orgId = null;
|
||||||
Guid? userId = null;
|
Guid? userId = null;
|
||||||
|
|
||||||
if (metaData.ContainsKey("organizationId"))
|
if (metaData.TryGetValue("organizationId", out var orgIdString))
|
||||||
{
|
{
|
||||||
orgId = new Guid(metaData["organizationId"]);
|
orgId = new Guid(orgIdString);
|
||||||
}
|
}
|
||||||
else if (metaData.ContainsKey("userId"))
|
else if (metaData.TryGetValue("userId", out var userIdString))
|
||||||
{
|
{
|
||||||
userId = new Guid(metaData["userId"]);
|
userId = new Guid(userIdString);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId == null && orgId == null)
|
if (userId != null && userId != Guid.Empty || orgId != null && orgId != Guid.Empty)
|
||||||
{
|
{
|
||||||
var orgIdKey = metaData.Keys.FirstOrDefault(k => k.ToLowerInvariant() == "organizationid");
|
return new Tuple<Guid?, Guid?>(orgId, userId);
|
||||||
if (!string.IsNullOrWhiteSpace(orgIdKey))
|
}
|
||||||
|
|
||||||
|
var orgIdKey = metaData.Keys
|
||||||
|
.FirstOrDefault(k => k.Equals("organizationid", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(orgIdKey))
|
||||||
|
{
|
||||||
|
orgId = new Guid(metaData[orgIdKey]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var userIdKey = metaData.Keys
|
||||||
|
.FirstOrDefault(k => k.Equals("userid", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(userIdKey))
|
||||||
{
|
{
|
||||||
orgId = new Guid(metaData[orgIdKey]);
|
userId = new Guid(metaData[userIdKey]);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var userIdKey = metaData.Keys.FirstOrDefault(k => k.ToLowerInvariant() == "userid");
|
|
||||||
if (!string.IsNullOrWhiteSpace(userIdKey))
|
|
||||||
{
|
|
||||||
userId = new Guid(metaData[userIdKey]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -891,9 +898,9 @@ public class StripeController : Controller
|
|||||||
return subscription;
|
return subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.DefaultTaxRates = new List<Stripe.TaxRate> { stripeTaxRate };
|
subscription.DefaultTaxRates = [stripeTaxRate];
|
||||||
|
|
||||||
var subscriptionOptions = new SubscriptionUpdateOptions { DefaultTaxRates = new List<string> { stripeTaxRate.Id } };
|
var subscriptionOptions = new SubscriptionUpdateOptions { DefaultTaxRates = [stripeTaxRate.Id] };
|
||||||
subscription = await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionOptions);
|
subscription = await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionOptions);
|
||||||
|
|
||||||
return subscription;
|
return subscription;
|
||||||
|
@ -25,7 +25,10 @@ public class PayPalIPNTransactionModel
|
|||||||
|
|
||||||
var data = queryString
|
var data = queryString
|
||||||
.AllKeys
|
.AllKeys
|
||||||
.ToDictionary(key => key, key => queryString[key]);
|
.Where(key => !string.IsNullOrWhiteSpace(key))
|
||||||
|
.ToDictionary(key =>
|
||||||
|
key.Trim('\r'),
|
||||||
|
key => queryString[key]?.Trim('\r'));
|
||||||
|
|
||||||
TransactionId = Extract(data, "txn_id");
|
TransactionId = Extract(data, "txn_id");
|
||||||
TransactionType = Extract(data, "txn_type");
|
TransactionType = Extract(data, "txn_type");
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Net;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Models;
|
using Bit.Core.Auth.Models;
|
||||||
@ -17,8 +18,14 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
public string Identifier { get; set; }
|
public string Identifier { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This value is HTML encoded. For display purposes use the method DisplayName() instead.
|
||||||
|
/// </summary>
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This value is HTML encoded. For display purposes use the method DisplayBusinessName() instead.
|
||||||
|
/// </summary>
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
public string BusinessName { get; set; }
|
public string BusinessName { get; set; }
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
@ -104,6 +111,22 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the name of the organization, HTML decoded ready for display.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName()
|
||||||
|
{
|
||||||
|
return WebUtility.HtmlDecode(Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the business name of the organization, HTML decoded ready for display.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayBusinessName()
|
||||||
|
{
|
||||||
|
return WebUtility.HtmlDecode(BusinessName);
|
||||||
|
}
|
||||||
|
|
||||||
public string BillingEmailAddress()
|
public string BillingEmailAddress()
|
||||||
{
|
{
|
||||||
return BillingEmail?.ToLowerInvariant()?.Trim();
|
return BillingEmail?.ToLowerInvariant()?.Trim();
|
||||||
@ -111,12 +134,12 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
|
|
||||||
public string BillingName()
|
public string BillingName()
|
||||||
{
|
{
|
||||||
return BusinessName;
|
return DisplayBusinessName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string SubscriberName()
|
public string SubscriberName()
|
||||||
{
|
{
|
||||||
return Name;
|
return DisplayName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string BraintreeCustomerIdPrefix()
|
public string BraintreeCustomerIdPrefix()
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using System.Net;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Entities.Provider;
|
namespace Bit.Core.AdminConsole.Entities.Provider;
|
||||||
@ -7,7 +9,13 @@ namespace Bit.Core.AdminConsole.Entities.Provider;
|
|||||||
public class Provider : ITableObject<Guid>
|
public class Provider : ITableObject<Guid>
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This value is HTML encoded. For display purposes use the method DisplayName() instead.
|
||||||
|
/// </summary>
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This value is HTML encoded. For display purposes use the method DisplayBusinessName() instead.
|
||||||
|
/// </summary>
|
||||||
public string BusinessName { get; set; }
|
public string BusinessName { get; set; }
|
||||||
public string BusinessAddress1 { get; set; }
|
public string BusinessAddress1 { get; set; }
|
||||||
public string BusinessAddress2 { get; set; }
|
public string BusinessAddress2 { get; set; }
|
||||||
@ -22,6 +30,9 @@ public class Provider : ITableObject<Guid>
|
|||||||
public bool Enabled { get; set; } = true;
|
public bool Enabled { get; set; } = true;
|
||||||
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
|
||||||
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
|
||||||
|
public GatewayType? Gateway { get; set; }
|
||||||
|
public string GatewayCustomerId { get; set; }
|
||||||
|
public string GatewaySubscriptionId { get; set; }
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
@ -30,4 +41,20 @@ public class Provider : ITableObject<Guid>
|
|||||||
Id = CoreHelpers.GenerateComb();
|
Id = CoreHelpers.GenerateComb();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the name of the provider, HTML decoded ready for display.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName()
|
||||||
|
{
|
||||||
|
return WebUtility.HtmlDecode(Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the business name of the provider, HTML decoded ready for display.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayBusinessName()
|
||||||
|
{
|
||||||
|
return WebUtility.HtmlDecode(BusinessName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
namespace Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
|
||||||
@ -6,6 +8,7 @@ public class OrganizationUserOrganizationDetails
|
|||||||
{
|
{
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
public Guid? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
public bool UseSso { get; set; }
|
public bool UseSso { get; set; }
|
||||||
@ -37,6 +40,7 @@ public class OrganizationUserOrganizationDetails
|
|||||||
public string PublicKey { get; set; }
|
public string PublicKey { get; set; }
|
||||||
public string PrivateKey { get; set; }
|
public string PrivateKey { get; set; }
|
||||||
public Guid? ProviderId { get; set; }
|
public Guid? ProviderId { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string ProviderName { get; set; }
|
public string ProviderName { get; set; }
|
||||||
public ProviderType? ProviderType { get; set; }
|
public ProviderType? ProviderType { get; set; }
|
||||||
public string FamilySponsorshipFriendlyName { get; set; }
|
public string FamilySponsorshipFriendlyName { get; set; }
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.Enums;
|
using System.Net;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
|
|
||||||
@ -7,6 +10,10 @@ public class ProviderOrganizationOrganizationDetails
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid ProviderId { get; set; }
|
public Guid ProviderId { get; set; }
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// This value is HTML encoded. For display purposes use the method DisplayName() instead.
|
||||||
|
/// </summary>
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string OrganizationName { get; set; }
|
public string OrganizationName { get; set; }
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
public string Settings { get; set; }
|
public string Settings { get; set; }
|
||||||
@ -16,4 +23,12 @@ public class ProviderOrganizationOrganizationDetails
|
|||||||
public int? Seats { get; set; }
|
public int? Seats { get; set; }
|
||||||
public string Plan { get; set; }
|
public string Plan { get; set; }
|
||||||
public OrganizationStatusType Status { get; set; }
|
public OrganizationStatusType Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the name of the organization, HTML decoded ready for display.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName()
|
||||||
|
{
|
||||||
|
return WebUtility.HtmlDecode(OrganizationName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
|
|
||||||
@ -7,6 +9,7 @@ public class ProviderOrganizationProviderDetails
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid ProviderId { get; set; }
|
public Guid ProviderId { get; set; }
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string ProviderName { get; set; }
|
public string ProviderName { get; set; }
|
||||||
public ProviderType ProviderType { get; set; }
|
public ProviderType ProviderType { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
|
|
||||||
@ -6,6 +8,7 @@ public class ProviderUserOrganizationDetails
|
|||||||
{
|
{
|
||||||
public Guid OrganizationId { get; set; }
|
public Guid OrganizationId { get; set; }
|
||||||
public Guid? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
public bool UseSso { get; set; }
|
public bool UseSso { get; set; }
|
||||||
@ -33,6 +36,7 @@ public class ProviderUserOrganizationDetails
|
|||||||
public string PrivateKey { get; set; }
|
public string PrivateKey { get; set; }
|
||||||
public Guid? ProviderId { get; set; }
|
public Guid? ProviderId { get; set; }
|
||||||
public Guid? ProviderUserId { get; set; }
|
public Guid? ProviderUserId { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string ProviderName { get; set; }
|
public string ProviderName { get; set; }
|
||||||
public Core.Enums.PlanType PlanType { get; set; }
|
public Core.Enums.PlanType PlanType { get; set; }
|
||||||
public bool LimitCollectionCreationDeletion { get; set; }
|
public bool LimitCollectionCreationDeletion { get; set; }
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
|
|
||||||
@ -6,6 +8,7 @@ public class ProviderUserProviderDetails
|
|||||||
{
|
{
|
||||||
public Guid ProviderId { get; set; }
|
public Guid ProviderId { get; set; }
|
||||||
public Guid? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Key { get; set; }
|
public string Key { get; set; }
|
||||||
public ProviderUserStatusType Status { get; set; }
|
public ProviderUserStatusType Status { get; set; }
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
namespace Bit.Core.AdminConsole.Models.Data.Provider;
|
||||||
|
|
||||||
@ -7,6 +9,7 @@ public class ProviderUserUserDetails
|
|||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public Guid ProviderId { get; set; }
|
public Guid ProviderId { get; set; }
|
||||||
public Guid? UserId { get; set; }
|
public Guid? UserId { get; set; }
|
||||||
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Email { get; set; }
|
public string Email { get; set; }
|
||||||
public ProviderUserStatusType Status { get; set; }
|
public ProviderUserStatusType Status { get; set; }
|
||||||
|
@ -537,6 +537,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
Storage = returnValue.Item1.MaxStorageGb,
|
Storage = returnValue.Item1.MaxStorageGb,
|
||||||
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
|
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
|
||||||
});
|
});
|
||||||
|
|
||||||
return returnValue;
|
return returnValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -819,7 +820,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Email = organization.BillingEmail,
|
Email = organization.BillingEmail,
|
||||||
Description = organization.BusinessName
|
Description = organization.DisplayBusinessName()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1276,7 +1277,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
orgUser.Email = null;
|
orgUser.Email = null;
|
||||||
|
|
||||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
|
||||||
await _mailService.SendOrganizationConfirmedEmailAsync(organization.Name, user.Email);
|
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email);
|
||||||
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
|
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
|
||||||
succeededUsers.Add(orgUser);
|
succeededUsers.Add(orgUser);
|
||||||
result.Add(Tuple.Create(orgUser, ""));
|
result.Add(Tuple.Create(orgUser, ""));
|
||||||
@ -1412,18 +1413,18 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the organization is using Flexible Collections, prevent use of any deprecated permissions
|
// If the organization is using Flexible Collections, prevent use of any deprecated permissions
|
||||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(user.OrganizationId);
|
var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId);
|
||||||
if (organizationAbility?.FlexibleCollections == true && user.Type == OrganizationUserType.Manager)
|
if (organization.FlexibleCollections && user.Type == OrganizationUserType.Manager)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("The Manager role has been deprecated by collection enhancements. Use the collection Can Manage permission instead.");
|
throw new BadRequestException("The Manager role has been deprecated by collection enhancements. Use the collection Can Manage permission instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (organizationAbility?.FlexibleCollections == true && user.AccessAll)
|
if (organization.FlexibleCollections && user.AccessAll)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("The AccessAll property has been deprecated by collection enhancements. Assign the user to collections instead.");
|
throw new BadRequestException("The AccessAll property has been deprecated by collection enhancements. Assign the user to collections instead.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (organizationAbility?.FlexibleCollections == true && collections?.Any() == true)
|
if (organization.FlexibleCollections && collections?.Any() == true)
|
||||||
{
|
{
|
||||||
var invalidAssociations = collections.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
|
var invalidAssociations = collections.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
|
||||||
if (invalidAssociations.Any())
|
if (invalidAssociations.Any())
|
||||||
@ -1440,7 +1441,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(user.OrganizationId, 1);
|
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(user.OrganizationId, 1);
|
||||||
if (additionalSmSeatsRequired > 0)
|
if (additionalSmSeatsRequired > 0)
|
||||||
{
|
{
|
||||||
var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId);
|
|
||||||
var update = new SecretsManagerSubscriptionUpdate(organization, true)
|
var update = new SecretsManagerSubscriptionUpdate(organization, true)
|
||||||
.AdjustSeats(additionalSmSeatsRequired);
|
.AdjustSeats(additionalSmSeatsRequired);
|
||||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
|
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
|
||||||
|
@ -124,14 +124,21 @@ public class PolicyService : IPolicyService
|
|||||||
switch (policy.Type)
|
switch (policy.Type)
|
||||||
{
|
{
|
||||||
case PolicyType.TwoFactorAuthentication:
|
case PolicyType.TwoFactorAuthentication:
|
||||||
foreach (var orgUser in removableOrgUsers)
|
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
|
||||||
|
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
|
||||||
{
|
{
|
||||||
if (!await userService.TwoFactorIsEnabledAsync(orgUser))
|
if (!await userService.TwoFactorIsEnabledAsync(orgUser))
|
||||||
{
|
{
|
||||||
|
if (!orgUser.HasMasterPassword)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
|
||||||
|
}
|
||||||
|
|
||||||
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
|
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
|
||||||
savingUserId);
|
savingUserId);
|
||||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||||
org.Name, orgUser.Email);
|
org.DisplayName(), orgUser.Email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -147,7 +154,7 @@ public class PolicyService : IPolicyService
|
|||||||
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
|
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
|
||||||
savingUserId);
|
savingUserId);
|
||||||
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
||||||
org.Name, orgUser.Email);
|
org.DisplayName(), orgUser.Email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Core.Context;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tokens;
|
using Bit.Core.Tokens;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Duo = DuoUniversal;
|
using Duo = DuoUniversal;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Identity;
|
namespace Bit.Core.Auth.Identity;
|
||||||
@ -25,6 +26,7 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
|
|||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory;
|
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory;
|
||||||
|
private readonly ILogger<TemporaryDuoWebV4SDKService> _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK
|
/// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK
|
||||||
@ -34,11 +36,13 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
|
|||||||
public TemporaryDuoWebV4SDKService(
|
public TemporaryDuoWebV4SDKService(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory)
|
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||||
|
ILogger<TemporaryDuoWebV4SDKService> logger)
|
||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_tokenDataFactory = tokenDataFactory;
|
_tokenDataFactory = tokenDataFactory;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -129,8 +133,9 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
|
|||||||
(string)provider.MetaData["Host"],
|
(string)provider.MetaData["Host"],
|
||||||
redirectUri).Build();
|
redirectUri).Build();
|
||||||
|
|
||||||
if (!await client.DoHealthCheck())
|
if (!await client.DoHealthCheck(true))
|
||||||
{
|
{
|
||||||
|
_logger.LogError("Unable to connect to Duo. Health check failed.");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return client;
|
return client;
|
||||||
|
@ -103,19 +103,27 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
|||||||
// established ownership in this context.
|
// established ownership in this context.
|
||||||
IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true);
|
IsUserHandleOwnerOfCredentialIdAsync callback = (args, cancellationToken) => Task.FromResult(true);
|
||||||
|
|
||||||
var res = await _fido2.MakeAssertionAsync(clientResponse, options, webAuthCred.Item2.PublicKey, webAuthCred.Item2.SignatureCounter, callback);
|
try
|
||||||
|
{
|
||||||
|
var res = await _fido2.MakeAssertionAsync(clientResponse, options, webAuthCred.Item2.PublicKey, webAuthCred.Item2.SignatureCounter, callback);
|
||||||
|
|
||||||
provider.MetaData.Remove("login");
|
provider.MetaData.Remove("login");
|
||||||
|
|
||||||
// Update SignatureCounter
|
// Update SignatureCounter
|
||||||
webAuthCred.Item2.SignatureCounter = res.Counter;
|
webAuthCred.Item2.SignatureCounter = res.Counter;
|
||||||
|
|
||||||
var providers = user.GetTwoFactorProviders();
|
var providers = user.GetTwoFactorProviders();
|
||||||
providers[TwoFactorProviderType.WebAuthn].MetaData[webAuthCred.Item1] = webAuthCred.Item2;
|
providers[TwoFactorProviderType.WebAuthn].MetaData[webAuthCred.Item1] = webAuthCred.Item2;
|
||||||
user.SetTwoFactorProviders(providers);
|
user.SetTwoFactorProviders(providers);
|
||||||
await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, logEvent: false);
|
await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, logEvent: false);
|
||||||
|
|
||||||
|
return res.Status == "ok";
|
||||||
|
}
|
||||||
|
catch (Fido2VerificationException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return res.Status == "ok";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasProperMetaData(TwoFactorProvider provider)
|
private bool HasProperMetaData(TwoFactorProvider provider)
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
namespace Bit.Core.Billing.Constants;
|
||||||
|
|
||||||
|
public static class StripeCustomerAutomaticTaxStatus
|
||||||
|
{
|
||||||
|
public const string Failed = "failed";
|
||||||
|
public const string NotCollecting = "not_collecting";
|
||||||
|
public const string Supported = "supported";
|
||||||
|
public const string UnrecognizedLocation = "unrecognized_location";
|
||||||
|
}
|
23
src/Core/Billing/Entities/ProviderPlan.cs
Normal file
23
src/Core/Billing/Entities/ProviderPlan.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Entities;
|
||||||
|
|
||||||
|
public class ProviderPlan : ITableObject<Guid>
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid ProviderId { get; set; }
|
||||||
|
public PlanType PlanType { get; set; }
|
||||||
|
public int? SeatMinimum { get; set; }
|
||||||
|
public int? PurchasedSeats { get; set; }
|
||||||
|
public int? AllocatedSeats { get; set; }
|
||||||
|
|
||||||
|
public void SetNewId()
|
||||||
|
{
|
||||||
|
if (Id == default)
|
||||||
|
{
|
||||||
|
Id = CoreHelpers.GenerateComb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
src/Core/Billing/Repositories/IProviderPlanRepository.cs
Normal file
9
src/Core/Billing/Repositories/IProviderPlanRepository.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using Bit.Core.Billing.Entities;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
|
||||||
|
namespace Bit.Core.Billing.Repositories;
|
||||||
|
|
||||||
|
public interface IProviderPlanRepository : IRepository<ProviderPlan, Guid>
|
||||||
|
{
|
||||||
|
Task<ProviderPlan> GetByProviderId(Guid providerId);
|
||||||
|
}
|
@ -108,7 +108,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string TrustedDeviceEncryption = "trusted-device-encryption";
|
public const string TrustedDeviceEncryption = "trusted-device-encryption";
|
||||||
public const string Fido2VaultCredentials = "fido2-vault-credentials";
|
public const string Fido2VaultCredentials = "fido2-vault-credentials";
|
||||||
public const string VaultOnboarding = "vault-onboarding";
|
public const string VaultOnboarding = "vault-onboarding";
|
||||||
public const string AutofillV2 = "autofill-v2";
|
|
||||||
public const string BrowserFilelessImport = "browser-fileless-import";
|
public const string BrowserFilelessImport = "browser-fileless-import";
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deprecated - never used, do not use. Will always default to false. Will be deleted as part of Flexible Collections cleanup
|
/// Deprecated - never used, do not use. Will always default to false. Will be deleted as part of Flexible Collections cleanup
|
||||||
@ -116,7 +115,6 @@ public static class FeatureFlagKeys
|
|||||||
public const string FlexibleCollections = "flexible-collections-disabled-do-not-use";
|
public const string FlexibleCollections = "flexible-collections-disabled-do-not-use";
|
||||||
public const string FlexibleCollectionsV1 = "flexible-collections-v-1"; // v-1 is intentional
|
public const string FlexibleCollectionsV1 = "flexible-collections-v-1"; // v-1 is intentional
|
||||||
public const string BulkCollectionAccess = "bulk-collection-access";
|
public const string BulkCollectionAccess = "bulk-collection-access";
|
||||||
public const string AutofillOverlay = "autofill-overlay";
|
|
||||||
public const string ItemShare = "item-share";
|
public const string ItemShare = "item-share";
|
||||||
public const string KeyRotationImprovements = "key-rotation-improvements";
|
public const string KeyRotationImprovements = "key-rotation-improvements";
|
||||||
public const string DuoRedirect = "duo-redirect";
|
public const string DuoRedirect = "duo-redirect";
|
||||||
@ -129,9 +127,10 @@ public static class FeatureFlagKeys
|
|||||||
/// flexible collections
|
/// flexible collections
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FlexibleCollectionsMigration = "flexible-collections-migration";
|
public const string FlexibleCollectionsMigration = "flexible-collections-migration";
|
||||||
public const string AC1607_PresentUsersWithOffboardingSurvey = "AC-1607_present-user-offboarding-survey";
|
|
||||||
public const string PM5766AutomaticTax = "PM-5766-automatic-tax";
|
public const string PM5766AutomaticTax = "PM-5766-automatic-tax";
|
||||||
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
|
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
|
||||||
|
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||||
|
public const string ShowPaymentMethodWarningBanners = "show-payment-method-warning-banners";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.52" />
|
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.59" />
|
||||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.52" />
|
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.59" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.2" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.2" />
|
||||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.15.0" />
|
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.15.0" />
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
|
||||||
@ -32,22 +32,22 @@
|
|||||||
<PackageReference Include="DnsClient" Version="1.7.0" />
|
<PackageReference Include="DnsClient" Version="1.7.0" />
|
||||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||||
<PackageReference Include="Handlebars.Net" Version="2.1.4" />
|
<PackageReference Include="Handlebars.Net" Version="2.1.4" />
|
||||||
<PackageReference Include="MailKit" Version="4.3.0" />
|
<PackageReference Include="MailKit" Version="4.4.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.25" />
|
||||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.38.0" />
|
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.38.0" />
|
||||||
<PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="1.0.8" />
|
<PackageReference Include="Microsoft.Azure.Cosmos.Table" Version="1.0.8" />
|
||||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.1.0" />
|
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.25" />
|
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="6.0.25" />
|
||||||
<PackageReference Include="Quartz" Version="3.4.0" />
|
<PackageReference Include="Quartz" Version="3.8.1" />
|
||||||
<PackageReference Include="SendGrid" Version="9.29.2" />
|
<PackageReference Include="SendGrid" Version="9.29.2" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||||
<PackageReference Include="Sentry.Serilog" Version="3.41.3" />
|
<PackageReference Include="Sentry.Serilog" Version="3.41.4" />
|
||||||
<PackageReference Include="Duende.IdentityServer" Version="6.3.7" />
|
<PackageReference Include="Duende.IdentityServer" Version="6.3.7" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="3.0.1" />
|
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="3.0.1" />
|
||||||
@ -57,7 +57,7 @@
|
|||||||
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
||||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.25" />
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="6.0.25" />
|
||||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.0.0" />
|
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -20,6 +20,7 @@ public class Transaction : ITableObject<Guid>
|
|||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
public string GatewayId { get; set; }
|
public string GatewayId { get; set; }
|
||||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public Guid? ProviderId { get; set; }
|
||||||
|
|
||||||
public void SetNewId()
|
public void SetNewId()
|
||||||
{
|
{
|
||||||
|
75
src/Core/MailTemplates/Handlebars/TrialInitiation.html.hbs
Normal file
75
src/Core/MailTemplates/Handlebars/TrialInitiation.html.hbs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
{{#>FullHtmlLayout}}
|
||||||
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
Welcome to Bitwarden! You can now get started with secure credential management and extend the benefits of end-to-end encryption across your entire organization.
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 10px; -webkit-text-size-adjust: none; text-align: center; border: 2px solid #175DDC; border-radius: 2px;" valign="top">
|
||||||
|
Your Master Password is <b>the only way</b> to unlock your account and only <b>you</b> hold the key. Memorize it, or write it down and keep it in a safe place.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="h3" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 18px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; font-weight: bold; padding: 0 0 5px;" valign="top">
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
Install Bitwarden
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
Access your Bitwarden account from anywhere and any device at <a href="{{{WebVaultUrl}}}/?utm_source=welcome_email&utm_medium=email" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">{{{WebVaultUrlHostname}}}</a>! For added convenience, <a href="https://bitwarden.com/download/" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">download and install Bitwarden</a> on any desktop, device, and browser.
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
<a href="https://bitwarden.com/download/" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: auto; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: none;">
|
||||||
|
<img src="https://bitwarden.com/images/mail-download-options.png" alt="Windows, Mac, Linux, Android, Apple, Chrome, Safari, Firefox, Edge, Opera, Brave, Vivaldi, Tor" width="598" height="65" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; margin: auto; border: none; max-width: 100%; height: auto; display: block;" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="h3" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 18px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; font-weight: bold; padding: 0 0 5px;" valign="top">
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
Securely Share using Bitwarden Organizations
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
Bitwarden makes it easy for teams and enterprises to securely share passwords, developer secrets, and passkeys via Organizations. Join an Organization if invited, or launch a new one anytime from the <a href="{{{WebVaultUrl}}}/?utm_source=welcome_email&utm_medium=email" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">Web App</a> with the <b>+ New Organization</b> button.
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top" align="center">
|
||||||
|
<a href="{{{WebVaultUrl}}}/?utm_source=welcome_email&utm_medium=email" clicktracking=off target="_blank" rel="noopener" style="color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
Login to Bitwarden
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="h3" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 18px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; font-weight: bold; padding: 0 0 5px;" valign="top">
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
Bitwarden is Here for You
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
|
||||||
|
Check out the <a href="http://www.bitwarden.com/help" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">Help</a> site for documentation, join the <a href="https://community.bitwarden.com/" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">Bitwarden Community</a> forums and connect with other enthusiasts on <a href="https://www.reddit.com/r/Bitwarden/" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">Reddit</a>. If you have any questions or issues, <a href="http://www.bitwarden.com/contact" target="_blank" rel="noopener" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #175DDC; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; text-decoration: underline;">contact support.</a>
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
|
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; font-weight: bold;" valign="top">
|
||||||
|
Stay safe and secure,<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||||
|
The Bitwarden Team
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{{/FullHtmlLayout}}
|
39
src/Core/MailTemplates/Handlebars/TrialInitiation.text.hbs
Normal file
39
src/Core/MailTemplates/Handlebars/TrialInitiation.text.hbs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{{#>FullTextLayout}}
|
||||||
|
Welcome to Bitwarden! You can now get started with secure credential management and extend the benefits of end-to-end encryption across your entire organization.
|
||||||
|
|
||||||
|
Your Master Password is the only way to unlock your account and only you hold the key. Memorize it, or write it down and keep it in a safe place.
|
||||||
|
|
||||||
|
|
||||||
|
Install Bitwarden
|
||||||
|
============
|
||||||
|
|
||||||
|
Access your Bitwarden account from anywhere and any device at the web vault ({{WebVaultUrl}}/?utm_source=welcome_email&utm_medium=email). For added convenience, download and install Bitwarden on any desktop, device and browser (http://www.bitwarden.com/download).
|
||||||
|
|
||||||
|
|
||||||
|
Download Options
|
||||||
|
============
|
||||||
|
|
||||||
|
http://www.bitwarden.com/download
|
||||||
|
|
||||||
|
|
||||||
|
Securely Share using Bitwarden Organizations
|
||||||
|
============
|
||||||
|
|
||||||
|
Bitwarden makes it easy for teams and enterprises to securely share passwords, developer secrets, and passkeys via Organizations. Join an Organization if invited, or launch a new one anytime from the Web App ({{WebVaultUrl}}/?utm_source=welcome_email&utm_medium=email) with the + New Organization button.
|
||||||
|
|
||||||
|
|
||||||
|
Login to Bitwarden
|
||||||
|
============
|
||||||
|
|
||||||
|
{{WebVaultUrl}}/?utm_source=welcome_email&utm_medium=email
|
||||||
|
|
||||||
|
|
||||||
|
Bitwarden is Here for You
|
||||||
|
============
|
||||||
|
|
||||||
|
Check out our Help (http://www.bitwarden.com/help) site for documentation, join the Bitwarden Community forums (https://community.bitwarden.com/) and connect with other enthusiasts on Reddit (https://www.reddit.com/r/Bitwarden/). If you have any questions or issues, contact support (http://www.bitwarden.com/contact).
|
||||||
|
|
||||||
|
|
||||||
|
Stay safe and secure,
|
||||||
|
The Bitwarden Team
|
||||||
|
{{/FullTextLayout}}
|
@ -42,6 +42,7 @@ public class SubscriptionInfo
|
|||||||
{
|
{
|
||||||
Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i));
|
Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i));
|
||||||
}
|
}
|
||||||
|
CollectionMethod = sub.CollectionMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DateTime? TrialStartDate { get; set; }
|
public DateTime? TrialStartDate { get; set; }
|
||||||
@ -54,6 +55,7 @@ public class SubscriptionInfo
|
|||||||
public string Status { get; set; }
|
public string Status { get; set; }
|
||||||
public bool Cancelled { get; set; }
|
public bool Cancelled { get; set; }
|
||||||
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
|
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
|
||||||
|
public string CollectionMethod { get; set; }
|
||||||
|
|
||||||
public class BillingSubscriptionItem
|
public class BillingSubscriptionItem
|
||||||
{
|
{
|
||||||
|
@ -15,7 +15,7 @@ public class OrganizationInvitesInfo
|
|||||||
bool initOrganization = false
|
bool initOrganization = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
OrganizationName = org.Name;
|
OrganizationName = org.DisplayName();
|
||||||
OrgSsoIdentifier = org.Identifier;
|
OrgSsoIdentifier = org.Identifier;
|
||||||
|
|
||||||
IsFreeOrg = org.PlanType == PlanType.Free;
|
IsFreeOrg = org.PlanType == PlanType.Free;
|
||||||
|
@ -65,6 +65,6 @@ public class SendSponsorshipOfferCommand : ISendSponsorshipOfferCommand
|
|||||||
throw new BadRequestException("Cannot find an outstanding sponsorship offer for this organization.");
|
throw new BadRequestException("Cannot find an outstanding sponsorship offer for this organization.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
|
await SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.DisplayName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,6 @@ public interface ITransactionRepository : IRepository<Transaction, Guid>
|
|||||||
{
|
{
|
||||||
Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId);
|
Task<ICollection<Transaction>> GetManyByUserIdAsync(Guid userId);
|
||||||
Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId);
|
Task<ICollection<Transaction>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||||
|
Task<ICollection<Transaction>> GetManyByProviderIdAsync(Guid providerId);
|
||||||
Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId);
|
Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId);
|
||||||
}
|
}
|
||||||
|
@ -77,5 +77,6 @@ public interface IMailService
|
|||||||
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||||
Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails);
|
||||||
Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier);
|
Task SendTrustedDeviceAdminApprovalEmailAsync(string email, DateTime utcNow, string ip, string deviceTypeAndIdentifier);
|
||||||
|
Task SendTrialInitiationEmailAsync(string email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ public class HandlebarsMailService : IMailService
|
|||||||
|
|
||||||
public async Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails)
|
public async Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable<string> ownerEmails)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage($"{organization.Name} Seat Count Has Increased", ownerEmails);
|
var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Count Has Increased", ownerEmails);
|
||||||
var model = new OrganizationSeatsAutoscaledViewModel
|
var model = new OrganizationSeatsAutoscaledViewModel
|
||||||
{
|
{
|
||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
@ -160,7 +160,7 @@ public class HandlebarsMailService : IMailService
|
|||||||
|
|
||||||
public async Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails)
|
public async Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable<string> ownerEmails)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage($"{organization.Name} Seat Limit Reached", ownerEmails);
|
var message = CreateDefaultMessage($"{organization.DisplayName()} Seat Limit Reached", ownerEmails);
|
||||||
var model = new OrganizationSeatsMaxReachedViewModel
|
var model = new OrganizationSeatsMaxReachedViewModel
|
||||||
{
|
{
|
||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
@ -179,7 +179,7 @@ public class HandlebarsMailService : IMailService
|
|||||||
var model = new OrganizationUserAcceptedViewModel
|
var model = new OrganizationUserAcceptedViewModel
|
||||||
{
|
{
|
||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
OrganizationName = CoreHelpers.SanitizeForEmail(organization.Name, false),
|
OrganizationName = CoreHelpers.SanitizeForEmail(organization.DisplayName(), false),
|
||||||
UserIdentifier = userIdentifier,
|
UserIdentifier = userIdentifier,
|
||||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||||
SiteName = _globalSettings.SiteName
|
SiteName = _globalSettings.SiteName
|
||||||
@ -251,6 +251,19 @@ public class HandlebarsMailService : IMailService
|
|||||||
await _mailDeliveryService.SendEmailAsync(message);
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SendTrialInitiationEmailAsync(string userEmail)
|
||||||
|
{
|
||||||
|
var message = CreateDefaultMessage("Welcome to Bitwarden!", userEmail);
|
||||||
|
var model = new BaseMailModel
|
||||||
|
{
|
||||||
|
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||||
|
SiteName = _globalSettings.SiteName
|
||||||
|
};
|
||||||
|
await AddMessageContentAsync(message, "TrialInitiation", model);
|
||||||
|
message.Category = "Welcome";
|
||||||
|
await _mailDeliveryService.SendEmailAsync(message);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
|
public async Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage("[Admin] Continue Logging In", email);
|
var message = CreateDefaultMessage("[Admin] Continue Logging In", email);
|
||||||
@ -920,7 +933,7 @@ public class HandlebarsMailService : IMailService
|
|||||||
public async Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
public async Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
||||||
IEnumerable<string> ownerEmails)
|
IEnumerable<string> ownerEmails)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage($"{organization.Name} Secrets Manager Seat Limit Reached", ownerEmails);
|
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Seat Limit Reached", ownerEmails);
|
||||||
var model = new OrganizationSeatsMaxReachedViewModel
|
var model = new OrganizationSeatsMaxReachedViewModel
|
||||||
{
|
{
|
||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
@ -935,7 +948,7 @@ public class HandlebarsMailService : IMailService
|
|||||||
public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
||||||
IEnumerable<string> ownerEmails)
|
IEnumerable<string> ownerEmails)
|
||||||
{
|
{
|
||||||
var message = CreateDefaultMessage($"{organization.Name} Secrets Manager Service Accounts Limit Reached", ownerEmails);
|
var message = CreateDefaultMessage($"{organization.DisplayName()} Secrets Manager Service Accounts Limit Reached", ownerEmails);
|
||||||
var model = new OrganizationServiceAccountsMaxReachedViewModel
|
var model = new OrganizationServiceAccountsMaxReachedViewModel
|
||||||
{
|
{
|
||||||
OrganizationId = organization.Id,
|
OrganizationId = organization.Id,
|
||||||
|
@ -132,13 +132,13 @@ public class LicensingService : ILicensingService
|
|||||||
{
|
{
|
||||||
_logger.LogInformation(Constants.BypassFiltersEventId, null,
|
_logger.LogInformation(Constants.BypassFiltersEventId, null,
|
||||||
"Organization {0} ({1}) has an invalid license and is being disabled. Reason: {2}",
|
"Organization {0} ({1}) has an invalid license and is being disabled. Reason: {2}",
|
||||||
org.Id, org.Name, reason);
|
org.Id, org.DisplayName(), reason);
|
||||||
org.Enabled = false;
|
org.Enabled = false;
|
||||||
org.ExpirationDate = license?.Expires ?? DateTime.UtcNow;
|
org.ExpirationDate = license?.Expires ?? DateTime.UtcNow;
|
||||||
org.RevisionDate = DateTime.UtcNow;
|
org.RevisionDate = DateTime.UtcNow;
|
||||||
await _organizationRepository.ReplaceAsync(org);
|
await _organizationRepository.ReplaceAsync(org);
|
||||||
|
|
||||||
await _mailService.SendLicenseExpiredAsync(new List<string> { org.BillingEmail }, org.Name);
|
await _mailService.SendLicenseExpiredAsync(new List<string> { org.BillingEmail }, org.DisplayName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ValidateUsersAsync()
|
public async Task ValidateUsersAsync()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -125,59 +126,61 @@ public class StripePaymentService : IPaymentService
|
|||||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
|
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
|
||||||
, additionalSmSeats, additionalServiceAccount);
|
, additionalSmSeats, additionalServiceAccount);
|
||||||
|
|
||||||
Stripe.Customer customer = null;
|
Customer customer = null;
|
||||||
Stripe.Subscription subscription;
|
Subscription subscription;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
var customerCreateOptions = new CustomerCreateOptions
|
||||||
{
|
{
|
||||||
Description = org.BusinessName,
|
Description = org.DisplayBusinessName(),
|
||||||
Email = org.BillingEmail,
|
Email = org.BillingEmail,
|
||||||
Source = stipeCustomerSourceToken,
|
Source = stipeCustomerSourceToken,
|
||||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||||
Metadata = stripeCustomerMetadata,
|
Metadata = stripeCustomerMetadata,
|
||||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||||
{
|
{
|
||||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
CustomFields =
|
||||||
{
|
[
|
||||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
new CustomerInvoiceSettingsCustomFieldOptions
|
||||||
{
|
{
|
||||||
Name = org.SubscriberType(),
|
Name = org.SubscriberType(),
|
||||||
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
||||||
},
|
}
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
Coupon = signupIsFromSecretsManagerTrial
|
Coupon = signupIsFromSecretsManagerTrial
|
||||||
? SecretsManagerStandaloneDiscountId
|
? SecretsManagerStandaloneDiscountId
|
||||||
: provider
|
: provider
|
||||||
? ProviderDiscountId
|
? ProviderDiscountId
|
||||||
: null,
|
: null,
|
||||||
Address = new Stripe.AddressOptions
|
Address = new AddressOptions
|
||||||
{
|
{
|
||||||
Country = taxInfo.BillingAddressCountry,
|
Country = taxInfo?.BillingAddressCountry,
|
||||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
PostalCode = taxInfo?.BillingAddressPostalCode,
|
||||||
// Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead.
|
// Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead.
|
||||||
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
|
Line1 = taxInfo?.BillingAddressLine1 ?? string.Empty,
|
||||||
Line2 = taxInfo.BillingAddressLine2,
|
Line2 = taxInfo?.BillingAddressLine2,
|
||||||
City = taxInfo.BillingAddressCity,
|
City = taxInfo?.BillingAddressCity,
|
||||||
State = taxInfo.BillingAddressState,
|
State = taxInfo?.BillingAddressState,
|
||||||
},
|
},
|
||||||
TaxIdData = !taxInfo.HasTaxId ? null : new List<Stripe.CustomerTaxIdDataOptions>
|
TaxIdData = taxInfo?.HasTaxId != true
|
||||||
{
|
? null
|
||||||
new Stripe.CustomerTaxIdDataOptions
|
:
|
||||||
{
|
[
|
||||||
Type = taxInfo.TaxIdType,
|
new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber, }
|
||||||
Value = taxInfo.TaxIdNumber,
|
],
|
||||||
},
|
};
|
||||||
},
|
|
||||||
});
|
customerCreateOptions.AddExpand("tax");
|
||||||
|
|
||||||
|
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||||
subCreateOptions.Customer = customer.Id;
|
subCreateOptions.Customer = customer.Id;
|
||||||
|
|
||||||
if (pm5766AutomaticTaxIsEnabled)
|
if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
|
||||||
{
|
{
|
||||||
subCreateOptions.AutomaticTax = new Stripe.SubscriptionAutomaticTaxOptions { Enabled = true };
|
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
||||||
@ -185,7 +188,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
|
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
|
||||||
{
|
{
|
||||||
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new Stripe.SubscriptionCancelOptions());
|
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions());
|
||||||
throw new GatewayException("Payment method was declined.");
|
throw new GatewayException("Payment method was declined.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,9 +255,10 @@ public class StripePaymentService : IPaymentService
|
|||||||
throw new BadRequestException("Organization already has a subscription.");
|
throw new BadRequestException("Organization already has a subscription.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var customerOptions = new Stripe.CustomerGetOptions();
|
var customerOptions = new CustomerGetOptions();
|
||||||
customerOptions.AddExpand("default_source");
|
customerOptions.AddExpand("default_source");
|
||||||
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
||||||
|
customerOptions.AddExpand("tax");
|
||||||
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions);
|
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions);
|
||||||
if (customer == null)
|
if (customer == null)
|
||||||
{
|
{
|
||||||
@ -301,14 +305,15 @@ public class StripePaymentService : IPaymentService
|
|||||||
var customerUpdateOptions = new CustomerUpdateOptions { Address = addressOptions };
|
var customerUpdateOptions = new CustomerUpdateOptions { Address = addressOptions };
|
||||||
customerUpdateOptions.AddExpand("default_source");
|
customerUpdateOptions.AddExpand("default_source");
|
||||||
customerUpdateOptions.AddExpand("invoice_settings.default_payment_method");
|
customerUpdateOptions.AddExpand("invoice_settings.default_payment_method");
|
||||||
|
customerUpdateOptions.AddExpand("tax");
|
||||||
customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions);
|
customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade);
|
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade);
|
||||||
|
|
||||||
if (pm5766AutomaticTaxIsEnabled)
|
if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
|
||||||
{
|
{
|
||||||
subCreateOptions.DefaultTaxRates = new List<string>();
|
subCreateOptions.DefaultTaxRates = [];
|
||||||
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,7 +338,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
|
private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
|
||||||
Stripe.Customer customer, Stripe.SubscriptionCreateOptions subCreateOptions)
|
Customer customer, SubscriptionCreateOptions subCreateOptions)
|
||||||
{
|
{
|
||||||
var stripePaymentMethod = false;
|
var stripePaymentMethod = false;
|
||||||
var paymentMethodType = PaymentMethodType.Credit;
|
var paymentMethodType = PaymentMethodType.Credit;
|
||||||
@ -351,12 +356,12 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
else if (customer.DefaultSource != null)
|
else if (customer.DefaultSource != null)
|
||||||
{
|
{
|
||||||
if (customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.SourceCard)
|
if (customer.DefaultSource is Card || customer.DefaultSource is SourceCard)
|
||||||
{
|
{
|
||||||
paymentMethodType = PaymentMethodType.Card;
|
paymentMethodType = PaymentMethodType.Card;
|
||||||
stripePaymentMethod = true;
|
stripePaymentMethod = true;
|
||||||
}
|
}
|
||||||
else if (customer.DefaultSource is Stripe.BankAccount || customer.DefaultSource is Stripe.SourceAchDebit)
|
else if (customer.DefaultSource is BankAccount || customer.DefaultSource is SourceAchDebit)
|
||||||
{
|
{
|
||||||
paymentMethodType = PaymentMethodType.BankAccount;
|
paymentMethodType = PaymentMethodType.BankAccount;
|
||||||
stripePaymentMethod = true;
|
stripePaymentMethod = true;
|
||||||
@ -394,7 +399,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var createdStripeCustomer = false;
|
var createdStripeCustomer = false;
|
||||||
Stripe.Customer customer = null;
|
Customer customer = null;
|
||||||
Braintree.Customer braintreeCustomer = null;
|
Braintree.Customer braintreeCustomer = null;
|
||||||
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount
|
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount
|
||||||
or PaymentMethodType.Credit;
|
or PaymentMethodType.Credit;
|
||||||
@ -422,14 +427,23 @@ public class StripePaymentService : IPaymentService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId);
|
var customerGetOptions = new CustomerGetOptions();
|
||||||
|
customerGetOptions.AddExpand("tax");
|
||||||
|
customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId, customerGetOptions);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Attempted to get existing customer from Stripe, but customer ID was not found. Attempting to recreate customer...");
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (customer == null && !string.IsNullOrWhiteSpace(paymentToken))
|
if (customer == null && !string.IsNullOrWhiteSpace(paymentToken))
|
||||||
{
|
{
|
||||||
var stripeCustomerMetadata = new Dictionary<string, string> { { "region", _globalSettings.BaseServiceUri.CloudRegion } };
|
var stripeCustomerMetadata = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
||||||
|
};
|
||||||
if (paymentMethodType == PaymentMethodType.PayPal)
|
if (paymentMethodType == PaymentMethodType.PayPal)
|
||||||
{
|
{
|
||||||
var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
||||||
@ -458,32 +472,35 @@ public class StripePaymentService : IPaymentService
|
|||||||
throw new GatewayException("Payment method is not supported at this time.");
|
throw new GatewayException("Payment method is not supported at this time.");
|
||||||
}
|
}
|
||||||
|
|
||||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
var customerCreateOptions = new CustomerCreateOptions
|
||||||
{
|
{
|
||||||
Description = user.Name,
|
Description = user.Name,
|
||||||
Email = user.Email,
|
Email = user.Email,
|
||||||
Metadata = stripeCustomerMetadata,
|
Metadata = stripeCustomerMetadata,
|
||||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||||
Source = stipeCustomerSourceToken,
|
Source = stipeCustomerSourceToken,
|
||||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||||
{
|
{
|
||||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
CustomFields =
|
||||||
{
|
[
|
||||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||||
{
|
{
|
||||||
Name = user.SubscriberType(),
|
Name = user.SubscriberType(),
|
||||||
Value = GetFirstThirtyCharacters(user.SubscriberName()),
|
Value = GetFirstThirtyCharacters(user.SubscriberName()),
|
||||||
},
|
}
|
||||||
}
|
|
||||||
|
]
|
||||||
},
|
},
|
||||||
Address = new Stripe.AddressOptions
|
Address = new AddressOptions
|
||||||
{
|
{
|
||||||
Line1 = string.Empty,
|
Line1 = string.Empty,
|
||||||
Country = taxInfo.BillingAddressCountry,
|
Country = taxInfo.BillingAddressCountry,
|
||||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
customerCreateOptions.AddExpand("tax");
|
||||||
|
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||||
createdStripeCustomer = true;
|
createdStripeCustomer = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -492,17 +509,17 @@ public class StripePaymentService : IPaymentService
|
|||||||
throw new GatewayException("Could not set up customer payment profile.");
|
throw new GatewayException("Could not set up customer payment profile.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var subCreateOptions = new Stripe.SubscriptionCreateOptions
|
var subCreateOptions = new SubscriptionCreateOptions
|
||||||
{
|
{
|
||||||
Customer = customer.Id,
|
Customer = customer.Id,
|
||||||
Items = new List<Stripe.SubscriptionItemOptions>(),
|
Items = [],
|
||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
[user.GatewayIdField()] = user.Id.ToString()
|
[user.GatewayIdField()] = user.Id.ToString()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions
|
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Plan = PremiumPlanId,
|
Plan = PremiumPlanId,
|
||||||
Quantity = 1
|
Quantity = 1
|
||||||
@ -524,25 +541,22 @@ public class StripePaymentService : IPaymentService
|
|||||||
var taxRate = taxRates.FirstOrDefault();
|
var taxRate = taxRates.FirstOrDefault();
|
||||||
if (taxRate != null)
|
if (taxRate != null)
|
||||||
{
|
{
|
||||||
subCreateOptions.DefaultTaxRates = new List<string>(1)
|
subCreateOptions.DefaultTaxRates = [taxRate.Id];
|
||||||
{
|
|
||||||
taxRate.Id
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (additionalStorageGb > 0)
|
if (additionalStorageGb > 0)
|
||||||
{
|
{
|
||||||
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions
|
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Plan = StoragePlanId,
|
Plan = StoragePlanId,
|
||||||
Quantity = additionalStorageGb
|
Quantity = additionalStorageGb
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pm5766AutomaticTaxIsEnabled)
|
if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
|
||||||
{
|
{
|
||||||
subCreateOptions.DefaultTaxRates = new List<string>();
|
subCreateOptions.DefaultTaxRates = [];
|
||||||
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -558,34 +572,33 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
user.Premium = true;
|
||||||
user.Premium = true;
|
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
||||||
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Stripe.Subscription> ChargeForNewSubscriptionAsync(ISubscriber subscriber, Stripe.Customer customer,
|
private async Task<Subscription> ChargeForNewSubscriptionAsync(ISubscriber subscriber, Customer customer,
|
||||||
bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType,
|
bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType,
|
||||||
Stripe.SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
|
SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
|
||||||
{
|
{
|
||||||
var addedCreditToStripeCustomer = false;
|
var addedCreditToStripeCustomer = false;
|
||||||
Braintree.Transaction braintreeTransaction = null;
|
Braintree.Transaction braintreeTransaction = null;
|
||||||
|
|
||||||
var subInvoiceMetadata = new Dictionary<string, string>();
|
var subInvoiceMetadata = new Dictionary<string, string>();
|
||||||
Stripe.Subscription subscription = null;
|
Subscription subscription = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!stripePaymentMethod)
|
if (!stripePaymentMethod)
|
||||||
{
|
{
|
||||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
|
||||||
{
|
{
|
||||||
Customer = customer.Id,
|
Customer = customer.Id,
|
||||||
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items)
|
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax))
|
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax) &&
|
||||||
|
CustomerHasTaxLocationVerified(customer))
|
||||||
{
|
{
|
||||||
previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true };
|
previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true };
|
||||||
}
|
}
|
||||||
@ -632,7 +645,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
throw new GatewayException("No payment was able to be collected.");
|
throw new GatewayException("No payment was able to be collected.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Balance = customer.Balance - previewInvoice.AmountDue
|
Balance = customer.Balance - previewInvoice.AmountDue
|
||||||
});
|
});
|
||||||
@ -649,10 +662,10 @@ public class StripePaymentService : IPaymentService
|
|||||||
};
|
};
|
||||||
|
|
||||||
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
||||||
if (pm5766AutomaticTaxIsEnabled)
|
if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
|
||||||
{
|
{
|
||||||
upcomingInvoiceOptions.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
upcomingInvoiceOptions.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
||||||
upcomingInvoiceOptions.SubscriptionDefaultTaxRates = new List<string>();
|
upcomingInvoiceOptions.SubscriptionDefaultTaxRates = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions);
|
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions);
|
||||||
@ -666,17 +679,12 @@ public class StripePaymentService : IPaymentService
|
|||||||
subCreateOptions.OffSession = true;
|
subCreateOptions.OffSession = true;
|
||||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax))
|
|
||||||
{
|
|
||||||
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
|
||||||
}
|
|
||||||
|
|
||||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
||||||
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
|
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
|
||||||
{
|
{
|
||||||
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
|
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
|
||||||
{
|
{
|
||||||
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new Stripe.SubscriptionCancelOptions());
|
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions());
|
||||||
throw new GatewayException("Payment method was declined.");
|
throw new GatewayException("Payment method was declined.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -694,7 +702,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
throw new GatewayException("Invoice not found.");
|
throw new GatewayException("Invoice not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions
|
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions
|
||||||
{
|
{
|
||||||
Metadata = subInvoiceMetadata
|
Metadata = subInvoiceMetadata
|
||||||
});
|
});
|
||||||
@ -712,7 +720,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
else if (addedCreditToStripeCustomer || customer.Balance < 0)
|
else if (addedCreditToStripeCustomer || customer.Balance < 0)
|
||||||
{
|
{
|
||||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Balance = customer.Balance
|
Balance = customer.Balance
|
||||||
});
|
});
|
||||||
@ -727,7 +735,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e is Stripe.StripeException strEx &&
|
if (e is StripeException strEx &&
|
||||||
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
||||||
{
|
{
|
||||||
throw new GatewayException("Bank account is not yet verified.");
|
throw new GatewayException("Bank account is not yet verified.");
|
||||||
@ -737,10 +745,10 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Stripe.InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
||||||
List<Stripe.SubscriptionItemOptions> subItemOptions)
|
List<SubscriptionItemOptions> subItemOptions)
|
||||||
{
|
{
|
||||||
return subItemOptions.Select(si => new Stripe.InvoiceSubscriptionItemOptions
|
return subItemOptions.Select(si => new InvoiceSubscriptionItemOptions
|
||||||
{
|
{
|
||||||
Plan = si.Plan,
|
Plan = si.Plan,
|
||||||
Price = si.Price,
|
Price = si.Price,
|
||||||
@ -753,7 +761,10 @@ public class StripePaymentService : IPaymentService
|
|||||||
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate, bool invoiceNow = false)
|
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate, bool invoiceNow = false)
|
||||||
{
|
{
|
||||||
// remember, when in doubt, throw
|
// remember, when in doubt, throw
|
||||||
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId);
|
var subGetOptions = new SubscriptionGetOptions();
|
||||||
|
// subGetOptions.AddExpand("customer");
|
||||||
|
subGetOptions.AddExpand("customer.tax");
|
||||||
|
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId, subGetOptions);
|
||||||
if (sub == null)
|
if (sub == null)
|
||||||
{
|
{
|
||||||
throw new GatewayException("Subscription not found.");
|
throw new GatewayException("Subscription not found.");
|
||||||
@ -766,7 +777,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
||||||
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
|
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
|
||||||
|
|
||||||
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions
|
var subUpdateOptions = new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
Items = updatedItemOptions,
|
Items = updatedItemOptions,
|
||||||
ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow
|
ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow
|
||||||
@ -777,7 +788,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
ProrationDate = prorationDate,
|
ProrationDate = prorationDate,
|
||||||
};
|
};
|
||||||
var immediatelyInvoice = false;
|
var immediatelyInvoice = false;
|
||||||
if (!invoiceNow && isPm5864DollarThresholdEnabled)
|
if (!invoiceNow && isPm5864DollarThresholdEnabled && sub.Status.Trim() != "trialing")
|
||||||
{
|
{
|
||||||
var upcomingInvoiceWithChanges = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
|
var upcomingInvoiceWithChanges = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
|
||||||
{
|
{
|
||||||
@ -789,7 +800,8 @@ public class StripePaymentService : IPaymentService
|
|||||||
SubscriptionBillingCycleAnchor = SubscriptionBillingCycleAnchor.Now
|
SubscriptionBillingCycleAnchor = SubscriptionBillingCycleAnchor.Now
|
||||||
});
|
});
|
||||||
|
|
||||||
immediatelyInvoice = upcomingInvoiceWithChanges.AmountRemaining >= 50000;
|
var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
|
||||||
|
immediatelyInvoice = isAnnualPlan && upcomingInvoiceWithChanges.AmountRemaining >= 50000;
|
||||||
|
|
||||||
subUpdateOptions.BillingCycleAnchor = immediatelyInvoice
|
subUpdateOptions.BillingCycleAnchor = immediatelyInvoice
|
||||||
? SubscriptionBillingCycleAnchor.Now
|
? SubscriptionBillingCycleAnchor.Now
|
||||||
@ -797,9 +809,11 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
||||||
if (pm5766AutomaticTaxIsEnabled)
|
if (pm5766AutomaticTaxIsEnabled &&
|
||||||
|
sub.AutomaticTax.Enabled != true &&
|
||||||
|
CustomerHasTaxLocationVerified(sub.Customer))
|
||||||
{
|
{
|
||||||
subUpdateOptions.DefaultTaxRates = new List<string>();
|
subUpdateOptions.DefaultTaxRates = [];
|
||||||
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -824,7 +838,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
var taxRate = taxRates.FirstOrDefault();
|
var taxRate = taxRates.FirstOrDefault();
|
||||||
if (taxRate != null && !sub.DefaultTaxRates.Any(x => x.Equals(taxRate.Id)))
|
if (taxRate != null && !sub.DefaultTaxRates.Any(x => x.Equals(taxRate.Id)))
|
||||||
{
|
{
|
||||||
subUpdateOptions.DefaultTaxRates = new List<string>(1) { taxRate.Id };
|
subUpdateOptions.DefaultTaxRates = [taxRate.Id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -834,7 +848,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);
|
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);
|
||||||
|
|
||||||
var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
|
var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new InvoiceGetOptions());
|
||||||
if (invoice == null)
|
if (invoice == null)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Unable to locate draft invoice for subscription update.");
|
throw new BadRequestException("Unable to locate draft invoice for subscription update.");
|
||||||
@ -852,11 +866,11 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions
|
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new InvoiceFinalizeOptions
|
||||||
{
|
{
|
||||||
AutoAdvance = false,
|
AutoAdvance = false,
|
||||||
});
|
});
|
||||||
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions());
|
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new InvoiceSendOptions());
|
||||||
paymentIntentClientSecret = null;
|
paymentIntentClientSecret = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -864,7 +878,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// Need to revert the subscription
|
// Need to revert the subscription
|
||||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
Items = subscriptionUpdate.RevertItemsOptions(sub),
|
Items = subscriptionUpdate.RevertItemsOptions(sub),
|
||||||
// This proration behavior prevents a false "credit" from
|
// This proration behavior prevents a false "credit" from
|
||||||
@ -889,7 +903,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
// Change back the subscription collection method and/or days until due
|
// Change back the subscription collection method and/or days until due
|
||||||
if (collectionMethod != "send_invoice" || daysUntilDue == null)
|
if (collectionMethod != "send_invoice" || daysUntilDue == null)
|
||||||
{
|
{
|
||||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new SubscriptionUpdateOptions
|
||||||
{
|
{
|
||||||
CollectionMethod = collectionMethod,
|
CollectionMethod = collectionMethod,
|
||||||
DaysUntilDue = daysUntilDue,
|
DaysUntilDue = daysUntilDue,
|
||||||
@ -950,7 +964,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
|
if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
|
||||||
{
|
{
|
||||||
await _stripeAdapter.SubscriptionCancelAsync(subscriber.GatewaySubscriptionId,
|
await _stripeAdapter.SubscriptionCancelAsync(subscriber.GatewaySubscriptionId,
|
||||||
new Stripe.SubscriptionCancelOptions());
|
new SubscriptionCancelOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
if (string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||||
@ -983,7 +997,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var charges = await _stripeAdapter.ChargeListAsync(new Stripe.ChargeListOptions
|
var charges = await _stripeAdapter.ChargeListAsync(new ChargeListOptions
|
||||||
{
|
{
|
||||||
Customer = subscriber.GatewayCustomerId
|
Customer = subscriber.GatewayCustomerId
|
||||||
});
|
});
|
||||||
@ -992,7 +1006,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
foreach (var charge in charges.Data.Where(c => c.Captured && !c.Refunded))
|
foreach (var charge in charges.Data.Where(c => c.Captured && !c.Refunded))
|
||||||
{
|
{
|
||||||
await _stripeAdapter.RefundCreateAsync(new Stripe.RefundCreateOptions { Charge = charge.Id });
|
await _stripeAdapter.RefundCreateAsync(new RefundCreateOptions { Charge = charge.Id });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1000,9 +1014,9 @@ public class StripePaymentService : IPaymentService
|
|||||||
await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId);
|
await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Stripe.Invoice invoice)
|
public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Invoice invoice)
|
||||||
{
|
{
|
||||||
var customerOptions = new Stripe.CustomerGetOptions();
|
var customerOptions = new CustomerGetOptions();
|
||||||
customerOptions.AddExpand("default_source");
|
customerOptions.AddExpand("default_source");
|
||||||
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
||||||
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
|
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
|
||||||
@ -1016,7 +1030,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card";
|
var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card";
|
||||||
var hasDefaultValidSource = customer.DefaultSource != null &&
|
var hasDefaultValidSource = customer.DefaultSource != null &&
|
||||||
(customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount);
|
(customer.DefaultSource is Card || customer.DefaultSource is BankAccount);
|
||||||
if (!hasDefaultCardPaymentMethod && !hasDefaultValidSource)
|
if (!hasDefaultCardPaymentMethod && !hasDefaultValidSource)
|
||||||
{
|
{
|
||||||
cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id;
|
cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id;
|
||||||
@ -1029,7 +1043,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions
|
await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new InvoiceFinalizeOptions
|
||||||
{
|
{
|
||||||
AutoAdvance = false
|
AutoAdvance = false
|
||||||
});
|
});
|
||||||
@ -1045,11 +1059,11 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
// Finalize the invoice (from Draft) w/o auto-advance so we
|
// Finalize the invoice (from Draft) w/o auto-advance so we
|
||||||
// can attempt payment manually.
|
// can attempt payment manually.
|
||||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions
|
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new InvoiceFinalizeOptions
|
||||||
{
|
{
|
||||||
AutoAdvance = false,
|
AutoAdvance = false,
|
||||||
});
|
});
|
||||||
var invoicePayOptions = new Stripe.InvoicePayOptions
|
var invoicePayOptions = new InvoicePayOptions
|
||||||
{
|
{
|
||||||
PaymentMethod = cardPaymentMethodId,
|
PaymentMethod = cardPaymentMethodId,
|
||||||
};
|
};
|
||||||
@ -1083,7 +1097,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
braintreeTransaction = transactionResult.Target;
|
braintreeTransaction = transactionResult.Target;
|
||||||
invoice = await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions
|
invoice = await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions
|
||||||
{
|
{
|
||||||
Metadata = new Dictionary<string, string>
|
Metadata = new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
@ -1099,13 +1113,13 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
invoice = await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions);
|
invoice = await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions);
|
||||||
}
|
}
|
||||||
catch (Stripe.StripeException e)
|
catch (StripeException e)
|
||||||
{
|
{
|
||||||
if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&
|
if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&
|
||||||
e.StripeError?.Code == "invoice_payment_intent_requires_action")
|
e.StripeError?.Code == "invoice_payment_intent_requires_action")
|
||||||
{
|
{
|
||||||
// SCA required, get intent client secret
|
// SCA required, get intent client secret
|
||||||
var invoiceGetOptions = new Stripe.InvoiceGetOptions();
|
var invoiceGetOptions = new InvoiceGetOptions();
|
||||||
invoiceGetOptions.AddExpand("payment_intent");
|
invoiceGetOptions.AddExpand("payment_intent");
|
||||||
invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions);
|
invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions);
|
||||||
paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret;
|
paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret;
|
||||||
@ -1130,7 +1144,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
return paymentIntentClientSecret;
|
return paymentIntentClientSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new Stripe.InvoiceVoidOptions());
|
invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new InvoiceVoidOptions());
|
||||||
|
|
||||||
// HACK: Workaround for customer balance credit
|
// HACK: Workaround for customer balance credit
|
||||||
if (invoice.StartingBalance < 0)
|
if (invoice.StartingBalance < 0)
|
||||||
@ -1143,7 +1157,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
// Assumption: Customer balance should now be $0, otherwise payment would not have failed.
|
// Assumption: Customer balance should now be $0, otherwise payment would not have failed.
|
||||||
if (customer.Balance == 0)
|
if (customer.Balance == 0)
|
||||||
{
|
{
|
||||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Balance = invoice.StartingBalance
|
Balance = invoice.StartingBalance
|
||||||
});
|
});
|
||||||
@ -1151,7 +1165,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e is Stripe.StripeException strEx &&
|
if (e is StripeException strEx &&
|
||||||
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
||||||
{
|
{
|
||||||
throw new GatewayException("Bank account is not yet verified.");
|
throw new GatewayException("Bank account is not yet verified.");
|
||||||
@ -1192,14 +1206,14 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
var canceledSub = endOfPeriod ?
|
var canceledSub = endOfPeriod ?
|
||||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
|
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
|
||||||
new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) :
|
new SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) :
|
||||||
await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new Stripe.SubscriptionCancelOptions());
|
await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new SubscriptionCancelOptions());
|
||||||
if (!canceledSub.CanceledAt.HasValue)
|
if (!canceledSub.CanceledAt.HasValue)
|
||||||
{
|
{
|
||||||
throw new GatewayException("Unable to cancel subscription.");
|
throw new GatewayException("Unable to cancel subscription.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Stripe.StripeException e)
|
catch (StripeException e)
|
||||||
{
|
{
|
||||||
if (e.Message != $"No such subscription: {subscriber.GatewaySubscriptionId}")
|
if (e.Message != $"No such subscription: {subscriber.GatewaySubscriptionId}")
|
||||||
{
|
{
|
||||||
@ -1233,7 +1247,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var updatedSub = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
|
var updatedSub = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
|
||||||
new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
|
new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
|
||||||
if (updatedSub.CanceledAt.HasValue)
|
if (updatedSub.CanceledAt.HasValue)
|
||||||
{
|
{
|
||||||
throw new GatewayException("Unable to reinstate subscription.");
|
throw new GatewayException("Unable to reinstate subscription.");
|
||||||
@ -1264,12 +1278,11 @@ public class StripePaymentService : IPaymentService
|
|||||||
};
|
};
|
||||||
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount;
|
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount;
|
||||||
|
|
||||||
Stripe.Customer customer = null;
|
Customer customer = null;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
var options = new Stripe.CustomerGetOptions();
|
var options = new CustomerGetOptions { Expand = ["sources", "tax", "subscriptions"] };
|
||||||
options.AddExpand("sources");
|
|
||||||
customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, options);
|
customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, options);
|
||||||
if (customer.Metadata?.Any() ?? false)
|
if (customer.Metadata?.Any() ?? false)
|
||||||
{
|
{
|
||||||
@ -1369,26 +1382,27 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
if (customer == null)
|
if (customer == null)
|
||||||
{
|
{
|
||||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions
|
||||||
{
|
{
|
||||||
Description = subscriber.BillingName(),
|
Description = subscriber.BillingName(),
|
||||||
Email = subscriber.BillingEmailAddress(),
|
Email = subscriber.BillingEmailAddress(),
|
||||||
Metadata = stripeCustomerMetadata,
|
Metadata = stripeCustomerMetadata,
|
||||||
Source = stipeCustomerSourceToken,
|
Source = stipeCustomerSourceToken,
|
||||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||||
{
|
{
|
||||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
CustomFields =
|
||||||
{
|
[
|
||||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||||
{
|
{
|
||||||
Name = subscriber.SubscriberType(),
|
Name = subscriber.SubscriberType(),
|
||||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()),
|
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()),
|
||||||
},
|
}
|
||||||
}
|
|
||||||
|
]
|
||||||
},
|
},
|
||||||
Address = taxInfo == null ? null : new Stripe.AddressOptions
|
Address = taxInfo == null ? null : new AddressOptions
|
||||||
{
|
{
|
||||||
Country = taxInfo.BillingAddressCountry,
|
Country = taxInfo.BillingAddressCountry,
|
||||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||||
@ -1397,7 +1411,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
City = taxInfo.BillingAddressCity,
|
City = taxInfo.BillingAddressCity,
|
||||||
State = taxInfo.BillingAddressState,
|
State = taxInfo.BillingAddressState,
|
||||||
},
|
},
|
||||||
Expand = new List<string> { "sources" },
|
Expand = ["sources", "tax", "subscriptions"],
|
||||||
});
|
});
|
||||||
|
|
||||||
subscriber.Gateway = GatewayType.Stripe;
|
subscriber.Gateway = GatewayType.Stripe;
|
||||||
@ -1413,7 +1427,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(stipeCustomerSourceToken) && paymentToken.StartsWith("btok_"))
|
if (!string.IsNullOrWhiteSpace(stipeCustomerSourceToken) && paymentToken.StartsWith("btok_"))
|
||||||
{
|
{
|
||||||
var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new Stripe.BankAccountCreateOptions
|
var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new BankAccountCreateOptions
|
||||||
{
|
{
|
||||||
Source = paymentToken
|
Source = paymentToken
|
||||||
});
|
});
|
||||||
@ -1422,7 +1436,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId))
|
else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId))
|
||||||
{
|
{
|
||||||
await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId,
|
await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId,
|
||||||
new Stripe.PaymentMethodAttachOptions { Customer = customer.Id });
|
new PaymentMethodAttachOptions { Customer = customer.Id });
|
||||||
defaultPaymentMethodId = stipeCustomerPaymentMethodId;
|
defaultPaymentMethodId = stipeCustomerPaymentMethodId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1431,44 +1445,44 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
foreach (var source in customer.Sources.Where(s => s.Id != defaultSourceId))
|
foreach (var source in customer.Sources.Where(s => s.Id != defaultSourceId))
|
||||||
{
|
{
|
||||||
if (source is Stripe.BankAccount)
|
if (source is BankAccount)
|
||||||
{
|
{
|
||||||
await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id);
|
await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id);
|
||||||
}
|
}
|
||||||
else if (source is Stripe.Card)
|
else if (source is Card)
|
||||||
{
|
{
|
||||||
await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id);
|
await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new Stripe.PaymentMethodListOptions
|
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new PaymentMethodListOptions
|
||||||
{
|
{
|
||||||
Customer = customer.Id,
|
Customer = customer.Id,
|
||||||
Type = "card"
|
Type = "card"
|
||||||
});
|
});
|
||||||
foreach (var cardMethod in cardPaymentMethods.Where(m => m.Id != defaultPaymentMethodId))
|
foreach (var cardMethod in cardPaymentMethods.Where(m => m.Id != defaultPaymentMethodId))
|
||||||
{
|
{
|
||||||
await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new Stripe.PaymentMethodDetachOptions());
|
await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new PaymentMethodDetachOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Metadata = stripeCustomerMetadata,
|
Metadata = stripeCustomerMetadata,
|
||||||
DefaultSource = defaultSourceId,
|
DefaultSource = defaultSourceId,
|
||||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||||
{
|
{
|
||||||
DefaultPaymentMethod = defaultPaymentMethodId,
|
DefaultPaymentMethod = defaultPaymentMethodId,
|
||||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
CustomFields =
|
||||||
{
|
[
|
||||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||||
{
|
{
|
||||||
Name = subscriber.SubscriberType(),
|
Name = subscriber.SubscriberType(),
|
||||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName())
|
Value = GetFirstThirtyCharacters(subscriber.SubscriberName())
|
||||||
},
|
}
|
||||||
}
|
]
|
||||||
},
|
},
|
||||||
Address = taxInfo == null ? null : new Stripe.AddressOptions
|
Address = taxInfo == null ? null : new AddressOptions
|
||||||
{
|
{
|
||||||
Country = taxInfo.BillingAddressCountry,
|
Country = taxInfo.BillingAddressCountry,
|
||||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||||
@ -1477,8 +1491,27 @@ public class StripePaymentService : IPaymentService
|
|||||||
City = taxInfo.BillingAddressCity,
|
City = taxInfo.BillingAddressCity,
|
||||||
State = taxInfo.BillingAddressState,
|
State = taxInfo.BillingAddressState,
|
||||||
},
|
},
|
||||||
|
Expand = ["tax", "subscriptions"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax) &&
|
||||||
|
!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) &&
|
||||||
|
customer.Subscriptions.Any(sub =>
|
||||||
|
sub.Id == subscriber.GatewaySubscriptionId &&
|
||||||
|
!sub.AutomaticTax.Enabled) &&
|
||||||
|
CustomerHasTaxLocationVerified(customer))
|
||||||
|
{
|
||||||
|
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
|
||||||
|
DefaultTaxRates = []
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = await _stripeAdapter.SubscriptionUpdateAsync(
|
||||||
|
subscriber.GatewaySubscriptionId,
|
||||||
|
subscriptionUpdateOptions);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -1494,7 +1527,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
|
|
||||||
public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount)
|
public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount)
|
||||||
{
|
{
|
||||||
Stripe.Customer customer = null;
|
Customer customer = null;
|
||||||
var customerExists = subscriber.Gateway == GatewayType.Stripe &&
|
var customerExists = subscriber.Gateway == GatewayType.Stripe &&
|
||||||
!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId);
|
!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId);
|
||||||
if (customerExists)
|
if (customerExists)
|
||||||
@ -1503,7 +1536,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions
|
||||||
{
|
{
|
||||||
Email = subscriber.BillingEmailAddress(),
|
Email = subscriber.BillingEmailAddress(),
|
||||||
Description = subscriber.BillingName(),
|
Description = subscriber.BillingName(),
|
||||||
@ -1511,7 +1544,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
subscriber.Gateway = GatewayType.Stripe;
|
subscriber.Gateway = GatewayType.Stripe;
|
||||||
subscriber.GatewayCustomerId = customer.Id;
|
subscriber.GatewayCustomerId = customer.Id;
|
||||||
}
|
}
|
||||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Balance = customer.Balance - (long)(creditAmount * 100)
|
Balance = customer.Balance - (long)(creditAmount * 100)
|
||||||
});
|
});
|
||||||
@ -1614,7 +1647,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId,
|
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId,
|
||||||
new Stripe.CustomerGetOptions { Expand = new List<string> { "tax_ids" } });
|
new CustomerGetOptions { Expand = ["tax_ids"] });
|
||||||
|
|
||||||
if (customer == null)
|
if (customer == null)
|
||||||
{
|
{
|
||||||
@ -1647,9 +1680,9 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new Stripe.CustomerUpdateOptions
|
var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new CustomerUpdateOptions
|
||||||
{
|
{
|
||||||
Address = new Stripe.AddressOptions
|
Address = new AddressOptions
|
||||||
{
|
{
|
||||||
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
|
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
|
||||||
Line2 = taxInfo.BillingAddressLine2,
|
Line2 = taxInfo.BillingAddressLine2,
|
||||||
@ -1658,7 +1691,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||||
Country = taxInfo.BillingAddressCountry,
|
Country = taxInfo.BillingAddressCountry,
|
||||||
},
|
},
|
||||||
Expand = new List<string> { "tax_ids" }
|
Expand = ["tax_ids"]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!subscriber.IsUser() && customer != null)
|
if (!subscriber.IsUser() && customer != null)
|
||||||
@ -1672,7 +1705,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) &&
|
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) &&
|
||||||
!string.IsNullOrWhiteSpace(taxInfo.TaxIdType))
|
!string.IsNullOrWhiteSpace(taxInfo.TaxIdType))
|
||||||
{
|
{
|
||||||
await _stripeAdapter.TaxIdCreateAsync(customer.Id, new Stripe.TaxIdCreateOptions
|
await _stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions
|
||||||
{
|
{
|
||||||
Type = taxInfo.TaxIdType,
|
Type = taxInfo.TaxIdType,
|
||||||
Value = taxInfo.TaxIdNumber,
|
Value = taxInfo.TaxIdNumber,
|
||||||
@ -1684,7 +1717,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
|
|
||||||
public async Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate)
|
public async Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate)
|
||||||
{
|
{
|
||||||
var stripeTaxRateOptions = new Stripe.TaxRateCreateOptions()
|
var stripeTaxRateOptions = new TaxRateCreateOptions()
|
||||||
{
|
{
|
||||||
DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}",
|
DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}",
|
||||||
Inclusive = false,
|
Inclusive = false,
|
||||||
@ -1717,7 +1750,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
|
|
||||||
var updatedStripeTaxRate = await _stripeAdapter.TaxRateUpdateAsync(
|
var updatedStripeTaxRate = await _stripeAdapter.TaxRateUpdateAsync(
|
||||||
taxRate.Id,
|
taxRate.Id,
|
||||||
new Stripe.TaxRateUpdateOptions() { Active = false }
|
new TaxRateUpdateOptions() { Active = false }
|
||||||
);
|
);
|
||||||
if (!updatedStripeTaxRate.Active)
|
if (!updatedStripeTaxRate.Active)
|
||||||
{
|
{
|
||||||
@ -1738,32 +1771,36 @@ public class StripePaymentService : IPaymentService
|
|||||||
{
|
{
|
||||||
var subscriptionInfo = await GetSubscriptionAsync(organization);
|
var subscriptionInfo = await GetSubscriptionAsync(organization);
|
||||||
|
|
||||||
if (subscriptionInfo.Subscription is not { Status: "active" or "trialing" or "past_due" } ||
|
if (subscriptionInfo.Subscription is not
|
||||||
subscriptionInfo.UpcomingInvoice == null)
|
{
|
||||||
|
Status: "active" or "trialing" or "past_due",
|
||||||
|
CollectionMethod: "charge_automatically"
|
||||||
|
}
|
||||||
|
|| subscriptionInfo.UpcomingInvoice == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var customer = await GetCustomerAsync(organization.GatewayCustomerId);
|
var customer = await GetCustomerAsync(organization.GatewayCustomerId, GetCustomerPaymentOptions());
|
||||||
|
|
||||||
var paymentSource = await GetBillingPaymentSourceAsync(customer);
|
var paymentSource = await GetBillingPaymentSourceAsync(customer);
|
||||||
|
|
||||||
return paymentSource == null;
|
return paymentSource == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Stripe.PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
private PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
||||||
{
|
{
|
||||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
|
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
|
||||||
new Stripe.PaymentMethodListOptions { Customer = customerId, Type = "card" });
|
new PaymentMethodListOptions { Customer = customerId, Type = "card" });
|
||||||
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
|
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
private decimal GetBillingBalance(Stripe.Customer customer)
|
private decimal GetBillingBalance(Customer customer)
|
||||||
{
|
{
|
||||||
return customer != null ? customer.Balance / 100M : default;
|
return customer != null ? customer.Balance / 100M : default;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Stripe.Customer customer)
|
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Customer customer)
|
||||||
{
|
{
|
||||||
if (customer == null)
|
if (customer == null)
|
||||||
{
|
{
|
||||||
@ -1792,7 +1829,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (customer.DefaultSource != null &&
|
if (customer.DefaultSource != null &&
|
||||||
(customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount))
|
(customer.DefaultSource is Card || customer.DefaultSource is BankAccount))
|
||||||
{
|
{
|
||||||
return new BillingInfo.BillingSource(customer.DefaultSource);
|
return new BillingInfo.BillingSource(customer.DefaultSource);
|
||||||
}
|
}
|
||||||
@ -1801,27 +1838,27 @@ public class StripePaymentService : IPaymentService
|
|||||||
return paymentMethod != null ? new BillingInfo.BillingSource(paymentMethod) : null;
|
return paymentMethod != null ? new BillingInfo.BillingSource(paymentMethod) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Stripe.CustomerGetOptions GetCustomerPaymentOptions()
|
private CustomerGetOptions GetCustomerPaymentOptions()
|
||||||
{
|
{
|
||||||
var customerOptions = new Stripe.CustomerGetOptions();
|
var customerOptions = new CustomerGetOptions();
|
||||||
customerOptions.AddExpand("default_source");
|
customerOptions.AddExpand("default_source");
|
||||||
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
||||||
return customerOptions;
|
return customerOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Stripe.Customer> GetCustomerAsync(string gatewayCustomerId, Stripe.CustomerGetOptions options = null)
|
private async Task<Customer> GetCustomerAsync(string gatewayCustomerId, CustomerGetOptions options = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(gatewayCustomerId))
|
if (string.IsNullOrWhiteSpace(gatewayCustomerId))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stripe.Customer customer = null;
|
Customer customer = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
customer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId, options);
|
customer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId, options);
|
||||||
}
|
}
|
||||||
catch (Stripe.StripeException) { }
|
catch (StripeException) { }
|
||||||
|
|
||||||
return customer;
|
return customer;
|
||||||
}
|
}
|
||||||
@ -1843,7 +1880,7 @@ public class StripePaymentService : IPaymentService
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<BillingInfo.BillingInvoice>> GetBillingInvoicesAsync(Stripe.Customer customer)
|
private async Task<IEnumerable<BillingInfo.BillingInvoice>> GetBillingInvoicesAsync(Customer customer)
|
||||||
{
|
{
|
||||||
if (customer == null)
|
if (customer == null)
|
||||||
{
|
{
|
||||||
@ -1865,28 +1902,32 @@ public class StripePaymentService : IPaymentService
|
|||||||
.OrderByDescending(invoice => invoice.Created)
|
.OrderByDescending(invoice => invoice.Created)
|
||||||
.Select(invoice => new BillingInfo.BillingInvoice(invoice));
|
.Select(invoice => new BillingInfo.BillingInvoice(invoice));
|
||||||
}
|
}
|
||||||
catch (Stripe.StripeException exception)
|
catch (StripeException exception)
|
||||||
{
|
{
|
||||||
_logger.LogError(exception, "An error occurred while listing Stripe invoices");
|
_logger.LogError(exception, "An error occurred while listing Stripe invoices");
|
||||||
throw new GatewayException("Failed to retrieve current invoices", exception);
|
throw new GatewayException("Failed to retrieve current invoices", exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a Stripe customer supports automatic tax
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="customer"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
private static bool CustomerHasTaxLocationVerified(Customer customer) =>
|
||||||
|
customer?.Tax?.AutomaticTax == StripeCustomerAutomaticTaxStatus.Supported;
|
||||||
|
|
||||||
// We are taking only first 30 characters of the SubscriberName because stripe provide
|
// We are taking only first 30 characters of the SubscriberName because stripe provide
|
||||||
// for 30 characters for custom_fields,see the link: https://stripe.com/docs/api/invoices/create
|
// for 30 characters for custom_fields,see the link: https://stripe.com/docs/api/invoices/create
|
||||||
public static string GetFirstThirtyCharacters(string subscriberName)
|
private static string GetFirstThirtyCharacters(string subscriberName)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(subscriberName))
|
if (string.IsNullOrWhiteSpace(subscriberName))
|
||||||
{
|
{
|
||||||
return "";
|
return string.Empty;
|
||||||
}
|
|
||||||
else if (subscriberName.Length <= 30)
|
|
||||||
{
|
|
||||||
return subscriberName;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return subscriberName.Substring(0, 30);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return subscriberName.Length <= 30
|
||||||
|
? subscriberName
|
||||||
|
: subscriberName[..30];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -338,14 +338,13 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
var result = await base.CreateAsync(user, masterPassword);
|
var result = await base.CreateAsync(user, masterPassword);
|
||||||
if (result == IdentityResult.Success)
|
if (result == IdentityResult.Success)
|
||||||
{
|
{
|
||||||
await _mailService.SendWelcomeEmailAsync(user);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(user.ReferenceData))
|
if (!string.IsNullOrEmpty(user.ReferenceData))
|
||||||
{
|
{
|
||||||
var referenceData = JsonConvert.DeserializeObject<Dictionary<string, object>>(user.ReferenceData);
|
var referenceData = JsonConvert.DeserializeObject<Dictionary<string, object>>(user.ReferenceData);
|
||||||
if (referenceData.TryGetValue("initiationPath", out var value))
|
if (referenceData.TryGetValue("initiationPath", out var value))
|
||||||
{
|
{
|
||||||
var initiationPath = value.ToString();
|
var initiationPath = value.ToString();
|
||||||
|
await SendAppropriateWelcomeEmailAsync(user, initiationPath);
|
||||||
if (!string.IsNullOrEmpty(initiationPath))
|
if (!string.IsNullOrEmpty(initiationPath))
|
||||||
{
|
{
|
||||||
await _referenceEventService.RaiseEventAsync(
|
await _referenceEventService.RaiseEventAsync(
|
||||||
@ -797,7 +796,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
user.ForcePasswordReset = true;
|
user.ForcePasswordReset = true;
|
||||||
|
|
||||||
await _userRepository.ReplaceAsync(user);
|
await _userRepository.ReplaceAsync(user);
|
||||||
await _mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.Name);
|
await _mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.DisplayName());
|
||||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_AdminResetPassword);
|
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_AdminResetPassword);
|
||||||
await _pushService.PushLogOutAsync(user.Id);
|
await _pushService.PushLogOutAsync(user.Id);
|
||||||
|
|
||||||
@ -1391,7 +1390,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
await organizationService.DeleteUserAsync(p.OrganizationId, user.Id);
|
await organizationService.DeleteUserAsync(p.OrganizationId, user.Id);
|
||||||
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
|
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
|
||||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||||
organization.Name, user.Email);
|
organization.DisplayName(), user.Email);
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
await Task.WhenAll(removeOrgUserTasks);
|
await Task.WhenAll(removeOrgUserTasks);
|
||||||
@ -1453,4 +1452,18 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
|||||||
|
|
||||||
return isVerified;
|
return isVerified;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SendAppropriateWelcomeEmailAsync(User user, string initiationPath)
|
||||||
|
{
|
||||||
|
var isFromMarketingWebsite = initiationPath.Contains("Secrets Manager trial");
|
||||||
|
|
||||||
|
if (isFromMarketingWebsite)
|
||||||
|
{
|
||||||
|
await _mailService.SendTrialInitiationEmailAsync(user.Email);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _mailService.SendWelcomeEmailAsync(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,5 +262,10 @@ public class NoopMailService : IMailService
|
|||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task SendTrialInitiationEmailAsync(string email)
|
||||||
|
{
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,6 +117,11 @@ public class Send : ITableObject<Guid>
|
|||||||
/// </value>
|
/// </value>
|
||||||
public bool? HideEmail { get; set; }
|
public bool? HideEmail { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies the Cipher associated with this send.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? CipherId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates the send's <see cref="Id" />
|
/// Generates the send's <see cref="Id" />
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Net;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using NS = Newtonsoft.Json;
|
using NS = Newtonsoft.Json;
|
||||||
@ -192,3 +193,33 @@ public class PermissiveStringEnumerableConverter : JsonConverter<IEnumerable<str
|
|||||||
writer.WriteEndArray();
|
writer.WriteEndArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Encodes incoming strings using HTML encoding
|
||||||
|
/// and decodes outgoing strings using HTML decoding.
|
||||||
|
/// </summary>
|
||||||
|
public class HtmlEncodingStringConverter : JsonConverter<string>
|
||||||
|
{
|
||||||
|
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
var originalValue = reader.GetString();
|
||||||
|
return WebUtility.HtmlEncode(originalValue);
|
||||||
|
}
|
||||||
|
return reader.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
var encodedValue = WebUtility.HtmlDecode(value);
|
||||||
|
writer.WriteStringValue(encodedValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -90,7 +90,12 @@ public class ClientStore : IClientStore
|
|||||||
|
|
||||||
private async Task<Client> CreateApiKeyClientAsync(string clientId)
|
private async Task<Client> CreateApiKeyClientAsync(string clientId)
|
||||||
{
|
{
|
||||||
var apiKey = await _apiKeyRepository.GetDetailsByIdAsync(new Guid(clientId));
|
if (!Guid.TryParse(clientId, out var guid))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiKey = await _apiKeyRepository.GetDetailsByIdAsync(guid);
|
||||||
|
|
||||||
if (apiKey == null || apiKey.ExpireAt <= DateTime.Now)
|
if (apiKey == null || apiKey.ExpireAt <= DateTime.Now)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
using System.Data;
|
||||||
|
using Bit.Core.Billing.Entities;
|
||||||
|
using Bit.Core.Billing.Repositories;
|
||||||
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Infrastructure.Dapper.Repositories;
|
||||||
|
using Dapper;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.Dapper.Billing.Repositories;
|
||||||
|
|
||||||
|
public class ProviderPlanRepository(
|
||||||
|
GlobalSettings globalSettings)
|
||||||
|
: Repository<ProviderPlan, Guid>(
|
||||||
|
globalSettings.SqlServer.ConnectionString,
|
||||||
|
globalSettings.SqlServer.ReadOnlyConnectionString), IProviderPlanRepository
|
||||||
|
{
|
||||||
|
public async Task<ProviderPlan> GetByProviderId(Guid providerId)
|
||||||
|
{
|
||||||
|
var sqlConnection = new SqlConnection(ConnectionString);
|
||||||
|
|
||||||
|
var results = await sqlConnection.QueryAsync<ProviderPlan>(
|
||||||
|
"[dbo].[ProviderPlan_ReadByProviderId]",
|
||||||
|
new { ProviderId = providerId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
return results.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,13 @@
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
using Bit.Core.Tools.Repositories;
|
using Bit.Core.Tools.Repositories;
|
||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Bit.Infrastructure.Dapper.AdminConsole.Repositories;
|
using Bit.Infrastructure.Dapper.AdminConsole.Repositories;
|
||||||
using Bit.Infrastructure.Dapper.Auth.Repositories;
|
using Bit.Infrastructure.Dapper.Auth.Repositories;
|
||||||
|
using Bit.Infrastructure.Dapper.Billing.Repositories;
|
||||||
using Bit.Infrastructure.Dapper.Repositories;
|
using Bit.Infrastructure.Dapper.Repositories;
|
||||||
using Bit.Infrastructure.Dapper.SecretsManager.Repositories;
|
using Bit.Infrastructure.Dapper.SecretsManager.Repositories;
|
||||||
using Bit.Infrastructure.Dapper.Tools.Repositories;
|
using Bit.Infrastructure.Dapper.Tools.Repositories;
|
||||||
@ -48,6 +50,7 @@ public static class DapperServiceCollectionExtensions
|
|||||||
services.AddSingleton<IUserRepository, UserRepository>();
|
services.AddSingleton<IUserRepository, UserRepository>();
|
||||||
services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>();
|
services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>();
|
||||||
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
|
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
|
||||||
|
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
|
||||||
|
|
||||||
if (selfHosted)
|
if (selfHosted)
|
||||||
{
|
{
|
||||||
|
@ -44,6 +44,16 @@ public class TransactionRepository : Repository<Transaction, Guid>, ITransaction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<Transaction>> GetManyByProviderIdAsync(Guid providerId)
|
||||||
|
{
|
||||||
|
await using var sqlConnection = new SqlConnection(ConnectionString);
|
||||||
|
var results = await sqlConnection.QueryAsync<Transaction>(
|
||||||
|
$"[{Schema}].[Transaction_ReadByProviderId]",
|
||||||
|
new { ProviderId = providerId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return results.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId)
|
public async Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId)
|
||||||
{
|
{
|
||||||
using (var connection = new SqlConnection(ConnectionString))
|
using (var connection = new SqlConnection(ConnectionString))
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
using Bit.Infrastructure.EntityFramework.Billing.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Billing.Configurations;
|
||||||
|
|
||||||
|
public class ProviderPlanEntityTypeConfiguration : IEntityTypeConfiguration<ProviderPlan>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ProviderPlan> builder)
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.Property(t => t.Id)
|
||||||
|
.ValueGeneratedNever();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.HasIndex(providerPlan => new { providerPlan.Id, providerPlan.PlanType })
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
builder.ToTable(nameof(ProviderPlan));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Billing.Models;
|
||||||
|
|
||||||
|
public class ProviderPlan : Core.Billing.Entities.ProviderPlan
|
||||||
|
{
|
||||||
|
public virtual Provider Provider { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProviderPlanMapperProfile : Profile
|
||||||
|
{
|
||||||
|
public ProviderPlanMapperProfile()
|
||||||
|
{
|
||||||
|
CreateMap<Core.Billing.Entities.ProviderPlan, ProviderPlan>().ReverseMap();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
using Bit.Core.Billing.Entities;
|
||||||
|
using Bit.Core.Billing.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using EFProviderPlan = Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Billing.Repositories;
|
||||||
|
|
||||||
|
public class ProviderPlanRepository(
|
||||||
|
IMapper mapper,
|
||||||
|
IServiceScopeFactory serviceScopeFactory)
|
||||||
|
: Repository<ProviderPlan, EFProviderPlan, Guid>(
|
||||||
|
serviceScopeFactory,
|
||||||
|
mapper,
|
||||||
|
context => context.ProviderPlans), IProviderPlanRepository
|
||||||
|
{
|
||||||
|
public async Task<ProviderPlan> GetByProviderId(Guid providerId)
|
||||||
|
{
|
||||||
|
using var serviceScope = ServiceScopeFactory.CreateScope();
|
||||||
|
var databaseContext = GetDatabaseContext(serviceScope);
|
||||||
|
var query =
|
||||||
|
from providerPlan in databaseContext.ProviderPlans
|
||||||
|
where providerPlan.ProviderId == providerId
|
||||||
|
select providerPlan;
|
||||||
|
return await query.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
|
using Bit.Core.Billing.Repositories;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.SecretsManager.Repositories;
|
using Bit.Core.SecretsManager.Repositories;
|
||||||
@ -7,6 +8,7 @@ using Bit.Core.Tools.Repositories;
|
|||||||
using Bit.Core.Vault.Repositories;
|
using Bit.Core.Vault.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Auth.Repositories;
|
using Bit.Infrastructure.EntityFramework.Auth.Repositories;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Billing.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories;
|
using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories;
|
||||||
using Bit.Infrastructure.EntityFramework.Tools.Repositories;
|
using Bit.Infrastructure.EntityFramework.Tools.Repositories;
|
||||||
@ -85,6 +87,7 @@ public static class EntityFrameworkServiceCollectionExtensions
|
|||||||
services.AddSingleton<IUserRepository, UserRepository>();
|
services.AddSingleton<IUserRepository, UserRepository>();
|
||||||
services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>();
|
services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>();
|
||||||
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
|
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
|
||||||
|
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
|
||||||
|
|
||||||
if (selfHosted)
|
if (selfHosted)
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||||
|
|
||||||
namespace Bit.Infrastructure.EntityFramework.Models;
|
namespace Bit.Infrastructure.EntityFramework.Models;
|
||||||
|
|
||||||
@ -7,6 +8,7 @@ public class Transaction : Core.Entities.Transaction
|
|||||||
{
|
{
|
||||||
public virtual Organization Organization { get; set; }
|
public virtual Organization Organization { get; set; }
|
||||||
public virtual User User { get; set; }
|
public virtual User User { get; set; }
|
||||||
|
public virtual Provider Provider { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TransactionMapperProfile : Profile
|
public class TransactionMapperProfile : Profile
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||||
using Bit.Infrastructure.EntityFramework.Auth.Models;
|
using Bit.Infrastructure.EntityFramework.Auth.Models;
|
||||||
|
using Bit.Infrastructure.EntityFramework.Billing.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.Converters;
|
using Bit.Infrastructure.EntityFramework.Converters;
|
||||||
using Bit.Infrastructure.EntityFramework.Models;
|
using Bit.Infrastructure.EntityFramework.Models;
|
||||||
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
||||||
@ -65,6 +66,7 @@ public class DatabaseContext : DbContext
|
|||||||
public DbSet<AuthRequest> AuthRequests { get; set; }
|
public DbSet<AuthRequest> AuthRequests { get; set; }
|
||||||
public DbSet<OrganizationDomain> OrganizationDomains { get; set; }
|
public DbSet<OrganizationDomain> OrganizationDomains { get; set; }
|
||||||
public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; }
|
public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; }
|
||||||
|
public DbSet<ProviderPlan> ProviderPlans { get; set; }
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
|
@ -47,4 +47,14 @@ public class TransactionRepository : Repository<Core.Entities.Transaction, Trans
|
|||||||
return Mapper.Map<List<Core.Entities.Transaction>>(results);
|
return Mapper.Map<List<Core.Entities.Transaction>>(results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<Core.Entities.Transaction>> GetManyByProviderIdAsync(Guid providerId)
|
||||||
|
{
|
||||||
|
using var serviceScope = ServiceScopeFactory.CreateScope();
|
||||||
|
var databaseContext = GetDatabaseContext(serviceScope);
|
||||||
|
var results = await databaseContext.Transactions
|
||||||
|
.Where(transaction => transaction.ProviderId == providerId)
|
||||||
|
.ToListAsync();
|
||||||
|
return Mapper.Map<List<Core.Entities.Transaction>>(results);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,8 @@
|
|||||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications' " />
|
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications' " />
|
||||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications-SelfHost' " />
|
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Notifications-SelfHost' " />
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.3" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.2" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
30
src/Sql/Billing/Stored Procedures/ProviderPlan_Create.sql
Normal file
30
src/Sql/Billing/Stored Procedures/ProviderPlan_Create.sql
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[ProviderPlan_Create]
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@ProviderId UNIQUEIDENTIFIER,
|
||||||
|
@PlanType TINYINT,
|
||||||
|
@SeatMinimum INT,
|
||||||
|
@PurchasedSeats INT,
|
||||||
|
@AllocatedSeats INT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[ProviderPlan]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[ProviderId],
|
||||||
|
[PlanType],
|
||||||
|
[SeatMinimum],
|
||||||
|
[PurchasedSeats],
|
||||||
|
[AllocatedSeats]
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Id,
|
||||||
|
@ProviderId,
|
||||||
|
@PlanType,
|
||||||
|
@SeatMinimum,
|
||||||
|
@PurchasedSeats,
|
||||||
|
@AllocatedSeats
|
||||||
|
)
|
||||||
|
END
|
@ -0,0 +1,12 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[ProviderPlan_DeleteById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[ProviderPlan]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
13
src/Sql/Billing/Stored Procedures/ProviderPlan_ReadById.sql
Normal file
13
src/Sql/Billing/Stored Procedures/ProviderPlan_ReadById.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[ProviderPlan_ReadById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[ProviderPlanView]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
END
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user