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.Storage.Blobs",
|
||||
"Azure.Storage.Queues",
|
||||
"DuoUniversal",
|
||||
"Fido2.AspNet",
|
||||
"Duende.IdentityServer",
|
||||
"Microsoft.Azure.Cosmos",
|
||||
|
33
.github/workflows/build.yml
vendored
33
.github/workflows/build.yml
vendored
@ -540,36 +540,11 @@ jobs:
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
(github.ref == 'refs/heads/main'
|
||||
|| github.ref == 'refs/heads/rc'
|
||||
|| github.ref == 'refs/heads/hotfix-rc'
|
||||
env:
|
||||
LINT_STATUS: ${{ needs.lint.result }}
|
||||
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
|
||||
|| github.ref == 'refs/heads/hotfix-rc')
|
||||
&& contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Log in to Azure - CI subscription
|
||||
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
|
||||
if: always()
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- purge
|
||||
needs: [purge]
|
||||
steps:
|
||||
- name: Check if any job failed
|
||||
if: |
|
||||
github.ref == 'refs/heads/main'
|
||||
(github.ref == 'refs/heads/main'
|
||||
|| github.ref == 'refs/heads/rc'
|
||||
|| github.ref == 'refs/heads/hotfix-rc'
|
||||
env:
|
||||
PURGE_STATUS: ${{ needs.purge.result }}
|
||||
run: |
|
||||
if [ "$PURGE_STATUS" = "failure" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|| github.ref == 'refs/heads/hotfix-rc')
|
||||
&& contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
|
||||
- name: Log in to Azure - CI subscription
|
||||
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"
|
||||
- "rc"
|
||||
- "hotfix-rc"
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
check-run:
|
||||
name: Check PR run
|
||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||
|
||||
sast:
|
||||
name: SAST scan
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-run
|
||||
permissions:
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@749fec53e0db0f6404a97e2e0807c3e80e3583a7 #2.0.23
|
||||
env:
|
||||
INCREMENTAL: "${{ github.event_name == 'pull_request' && '--sast-incremental' || '' }}"
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
project_name: ${{ github.repository }}
|
||||
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
|
||||
@ -35,17 +43,21 @@ jobs:
|
||||
additional_params: --report-format sarif --output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- 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:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
quality:
|
||||
name: Quality scan
|
||||
runs-on: ubuntu-22.04
|
||||
needs: check-run
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with SonarCloud
|
||||
uses: sonarsource/sonarcloud-github-action@49e6cd3b187936a73b8280d59ffd9da69df63ec9 # v2.1.1
|
||||
@ -56,5 +68,4 @@ jobs:
|
||||
args: >
|
||||
-Dsonar.organization=${{ github.repository_owner }}
|
||||
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
|
||||
-Dsonar.test.exclusions=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
|
||||
|
||||
- name: Migrate SQL Server
|
||||
working-directory: "dev"
|
||||
run: "./migrate.ps1"
|
||||
shell: pwsh
|
||||
run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "Server=localhost;Database=vault_dev;User Id=SA;Password=SET_A_PASSWORD_HERE_123;Encrypt=True;TrustServerCertificate=True;"
|
||||
|
||||
- name: Migrate MySQL
|
||||
working-directory: "util/MySqlMigrations"
|
||||
@ -147,9 +147,9 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Migrate
|
||||
working-directory: "dev"
|
||||
run: "./migrate.ps1"
|
||||
shell: pwsh
|
||||
run: 'dotnet run --project util/MsSqlMigratorUtility/ "$CONN_STR"'
|
||||
env:
|
||||
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
|
||||
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
|
||||
run-name: Bump version to ${{ inputs.version_number }}
|
||||
name: Version Bump
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_number:
|
||||
description: "New version (example: '2024.1.0')"
|
||||
required: true
|
||||
version_number_override:
|
||||
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
||||
required: false
|
||||
type: string
|
||||
cut_rc_branch:
|
||||
description: "Cut RC branch?"
|
||||
@ -16,22 +15,16 @@ on:
|
||||
|
||||
jobs:
|
||||
bump_version:
|
||||
name: Bump
|
||||
name: Bump Version
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
version: ${{ steps.set-final-version-output.outputs.version }}
|
||||
steps:
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||
- name: Validate version input
|
||||
if: ${{ inputs.version_number_override != '' }}
|
||||
uses: bitwarden/gh-actions/version-check@main
|
||||
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"
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
@ -48,6 +41,20 @@ jobs:
|
||||
exit 1
|
||||
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
|
||||
uses: crazy-max/ghaction-import-gpg@82a020f1f7f605c65dd2449b392a52c3fcfef7ef # v6.0.0
|
||||
with:
|
||||
@ -56,22 +63,35 @@ jobs:
|
||||
git_user_signingkey: 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
|
||||
id: create-branch
|
||||
run: |
|
||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
||||
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
|
||||
git switch -c $NAME
|
||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- 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
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
- name: Get current version
|
||||
id: current-version
|
||||
run: |
|
||||
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.
|
||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||
echo "Version has not changed."
|
||||
@ -87,16 +107,37 @@ jobs:
|
||||
exit 1
|
||||
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
|
||||
with:
|
||||
version: ${{ inputs.version_number }}
|
||||
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: |
|
||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||
git config --local user.name "bitwarden-devops-bot"
|
||||
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
|
||||
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
|
||||
id: version-changed
|
||||
@ -110,7 +151,7 @@ jobs:
|
||||
|
||||
- name: Commit files
|
||||
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
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
@ -124,7 +165,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
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: |
|
||||
PR_URL=$(gh pr create --title "$TITLE" \
|
||||
--base "main" \
|
||||
@ -140,38 +181,43 @@ jobs:
|
||||
- [X] Other
|
||||
|
||||
## 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
|
||||
|
||||
- name: Approve PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
run: gh pr review $PR_NUMBER --approve
|
||||
|
||||
- name: Merge PR
|
||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||
|
||||
|
||||
cut_rc:
|
||||
name: Cut RC branch
|
||||
needs: bump_version
|
||||
if: ${{ inputs.cut_rc_branch == true }}
|
||||
needs: bump_version
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: main
|
||||
|
||||
|
||||
- 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
|
||||
env:
|
||||
NEW_VERSION: ${{ inputs.version_number }}
|
||||
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
|
||||
run: |
|
||||
# Wait for version to change.
|
||||
while : ; do
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2024.2.3</Version>
|
||||
<Version>2024.3.0</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -17,6 +17,7 @@ using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Commercial.Core.AdminConsole.Services;
|
||||
|
||||
@ -257,7 +258,7 @@ public class ProviderService : IProviderService
|
||||
|
||||
await _providerUserRepository.ReplaceAsync(providerUser);
|
||||
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, ""));
|
||||
}
|
||||
catch (BadRequestException e)
|
||||
@ -331,7 +332,7 @@ public class ProviderService : IProviderService
|
||||
var email = user == null ? providerUser.Email : user.Email;
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
await _mailService.SendProviderUserRemoved(provider.Name, email);
|
||||
await _mailService.SendProviderUserRemoved(provider.DisplayName(), email);
|
||||
}
|
||||
|
||||
result.Add(Tuple.Create(providerUser, ""));
|
||||
@ -374,8 +375,18 @@ public class ProviderService : IProviderService
|
||||
Key = key,
|
||||
};
|
||||
|
||||
await ApplyProviderPriceRateAsync(organizationId, providerId);
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
|
||||
await ApplyProviderPriceRateAsync(organization, provider);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -400,16 +411,14 @@ public class ProviderService : IProviderService
|
||||
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 (provider.CreationDate >= Constants.ProviderCreatedPriorNov62023)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, GetStripeSeatPlanId(organization.PlanType));
|
||||
var extractedPlanType = PlanTypeMappings(organization);
|
||||
if (subscriptionItem != null)
|
||||
@ -586,7 +595,7 @@ public class ProviderService : IProviderService
|
||||
var nowMillis = CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow);
|
||||
var token = _dataProtector.Protect(
|
||||
$"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)
|
||||
|
@ -26,6 +26,7 @@ using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AuthenticationSchemes = Bit.Core.AuthenticationSchemes;
|
||||
using DIM = Duende.IdentityServer.Models;
|
||||
|
||||
namespace Bit.Sso.Controllers;
|
||||
@ -483,7 +484,7 @@ public class AccountController : Controller
|
||||
if (orgUser.Status == OrganizationUserStatusType.Invited)
|
||||
{
|
||||
// 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;
|
||||
@ -516,7 +517,7 @@ public class AccountController : Controller
|
||||
await _organizationService.AdjustSeatsAsync(orgId, initialSeatCount - organization.Seats.Value, prorationDate);
|
||||
}
|
||||
_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;
|
||||
|
||||
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);
|
||||
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 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.Any<ProviderOrganization>(),
|
||||
.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]
|
||||
@ -576,65 +671,6 @@ public class ProviderServiceTests
|
||||
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) =>
|
||||
new()
|
||||
{
|
||||
|
@ -1,12 +1,12 @@
|
||||
#!/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.
|
||||
# The best workaround seems to be adding tiny delay like so:
|
||||
sleep 0.1;
|
||||
|
||||
MIGRATE_DIRECTORY="/mnt/migrator/DbScripts"
|
||||
LAST_MIGRATION_FILE="/mnt/data/last_migration"
|
||||
SERVER='mssql'
|
||||
DATABASE="vault_dev"
|
||||
USER="SA"
|
||||
@ -16,58 +16,33 @@ while getopts "s" arg; do
|
||||
case $arg in
|
||||
s)
|
||||
echo "Running for self-host environment"
|
||||
LAST_MIGRATION_FILE="/mnt/data/last_self_host_migration"
|
||||
DATABASE="vault_dev_self_host"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ! -f "$LAST_MIGRATION_FILE" ]; then
|
||||
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')
|
||||
QUERY="IF OBJECT_ID('[$DATABASE].[dbo].[Migration]') IS NULL AND OBJECT_ID('[migrations_$DATABASE].[dbo].[migrations]') IS NOT NULL
|
||||
BEGIN
|
||||
CREATE DATABASE migrations_$DATABASE;
|
||||
END;
|
||||
-- Create [database].dbo.Migration with the schema expected by MsSqlMigratorUtility
|
||||
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"
|
||||
|
||||
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.
|
||||
|
||||
# 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
|
||||
# in the future and investigate if we can migrate back.
|
||||
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
|
||||
# the mssql-tools container which runs under x86_64.
|
||||
|
||||
param(
|
||||
[switch]$all = $false,
|
||||
[switch]$postgres = $false,
|
||||
[switch]$mysql = $false,
|
||||
[switch]$mssql = $false,
|
||||
[switch]$sqlite = $false,
|
||||
[switch]$selfhost = $false,
|
||||
[switch]$pipeline = $false
|
||||
[switch]$all,
|
||||
[switch]$postgres,
|
||||
[switch]$mysql,
|
||||
[switch]$mssql,
|
||||
[switch]$sqlite,
|
||||
[switch]$selfhost
|
||||
)
|
||||
|
||||
# Abort on any error
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
|
||||
$mssql = $true;
|
||||
}
|
||||
@ -29,22 +29,27 @@ if ($all -or $postgres -or $mysql -or $sqlite) {
|
||||
}
|
||||
|
||||
if ($all -or $mssql) {
|
||||
if ($selfhost) {
|
||||
$migrationArgs = "-s"
|
||||
} elseif ($pipeline) {
|
||||
$migrationArgs = "-p"
|
||||
function Get-UserSecrets {
|
||||
return dotnet user-secrets list --json --project ../src/Api | ConvertFrom-Json
|
||||
}
|
||||
|
||||
Write-Host "Starting Microsoft SQL Server Migrations"
|
||||
docker run `
|
||||
-v "$(pwd)/helpers/mssql:/mnt/helpers" `
|
||||
-v "$(pwd)/../util/Migrator:/mnt/migrator/" `
|
||||
-v "$(pwd)/.data/mssql:/mnt/data" `
|
||||
--env-file .env `
|
||||
--network=bitwardenserver_default `
|
||||
--rm `
|
||||
mcr.microsoft.com/mssql-tools `
|
||||
/mnt/helpers/run_migrations.sh $migrationArgs
|
||||
if ($selfhost) {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
|
||||
$envName = "self-host"
|
||||
|
||||
Write-Output "Migrating your migrations to use MsSqlMigratorUtility (if needed)"
|
||||
./migrate_migration_record.ps1 -s
|
||||
} else {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
|
||||
$envName = "cloud"
|
||||
|
||||
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
|
||||
|
@ -1,15 +1,13 @@
|
||||
#!/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
|
||||
# in a file to recording migrations in a database table. It will create a
|
||||
# migrations_vault table and store all of the previously run migrations as
|
||||
# indicated by a last_migrations file. It will then delete this file.
|
||||
# This is a migration script to move data from [migrations_vault_dev].[dbo].[migrations] (used by our custom
|
||||
# migrator script) to [vault_dev].[dbo].[Migration] (used by MsSqlMigratorUtility). It is safe to run multiple
|
||||
# times because it will not perform any migration if it detects that the new table is already present.
|
||||
# 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
|
||||
# the mssql-tools container which runs under x86_64. We should monitor this
|
||||
# in the future and investigate if we can migrate back.
|
||||
# docker-compose --profile mssql exec mssql bash /mnt/helpers/run_migrations.sh @args
|
||||
# the mssql-tools container which runs under x86_64.
|
||||
|
||||
docker run `
|
||||
-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.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
@ -119,8 +120,9 @@ public class OrganizationsController : Controller
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var encodedName = WebUtility.HtmlEncode(name);
|
||||
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
|
||||
{
|
||||
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.Utilities;
|
||||
using Bit.Core;
|
||||
@ -188,8 +189,9 @@ public class ProvidersController : Controller
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var encodedName = WebUtility.HtmlEncode(name);
|
||||
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
|
||||
{
|
||||
OrganizationName = string.IsNullOrWhiteSpace(name) ? null : name,
|
||||
@ -199,7 +201,7 @@ public class ProvidersController : Controller
|
||||
Items = unassignedOrganizations.Select(uo => new OrganizationSelectableViewModel
|
||||
{
|
||||
Id = uo.Id,
|
||||
Name = uo.Name,
|
||||
Name = uo.DisplayName(),
|
||||
PlanType = uo.PlanType
|
||||
}).ToList()
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -36,8 +37,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
BillingInfo = billingInfo;
|
||||
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
|
||||
Name = org.Name;
|
||||
BusinessName = org.BusinessName;
|
||||
Name = org.DisplayName();
|
||||
BusinessName = org.DisplayBusinessName();
|
||||
BillingEmail = provider?.Type == ProviderType.Reseller ? provider.BillingEmail : org.BillingEmail;
|
||||
PlanType = org.PlanType;
|
||||
Plan = org.Plan;
|
||||
@ -184,8 +185,8 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
|
||||
public Organization ToOrganization(Organization existingOrganization)
|
||||
{
|
||||
existingOrganization.Name = Name;
|
||||
existingOrganization.BusinessName = BusinessName;
|
||||
existingOrganization.Name = WebUtility.HtmlEncode(Name.Trim());
|
||||
existingOrganization.BusinessName = WebUtility.HtmlEncode(BusinessName.Trim());
|
||||
existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
existingOrganization.PlanType = PlanType.Value;
|
||||
existingOrganization.Plan = Plan;
|
||||
|
@ -11,8 +11,8 @@ public class ProviderEditModel : ProviderViewModel
|
||||
public ProviderEditModel(Provider provider, IEnumerable<ProviderUserUserDetails> providerUsers, IEnumerable<ProviderOrganizationOrganizationDetails> organizations)
|
||||
: base(provider, providerUsers, organizations)
|
||||
{
|
||||
Name = provider.Name;
|
||||
BusinessName = provider.BusinessName;
|
||||
Name = provider.DisplayName();
|
||||
BusinessName = provider.DisplayBusinessName();
|
||||
BillingEmail = provider.BillingEmail;
|
||||
BillingPhone = provider.BillingPhone;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@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 canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
|
||||
@ -58,7 +58,7 @@
|
||||
</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)
|
||||
{
|
||||
|
@ -46,7 +46,7 @@
|
||||
{
|
||||
<tr>
|
||||
<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>
|
||||
@org.Plan
|
||||
|
@ -1,10 +1,10 @@
|
||||
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
|
||||
@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)
|
||||
{
|
||||
|
@ -2,8 +2,8 @@
|
||||
@model Bit.Core.AdminConsole.Entities.Provider.Provider
|
||||
<dl class="row">
|
||||
<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>
|
||||
<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.CheckBoxFor(m => Model.Items[i].Selected)
|
||||
</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>
|
||||
</tr>
|
||||
}
|
||||
|
@ -3,12 +3,12 @@
|
||||
|
||||
@model ProviderEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.Name;
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
|
||||
|
||||
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>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
@ -17,12 +17,12 @@
|
||||
<h2>General</h2>
|
||||
<dl class="row">
|
||||
<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>
|
||||
<h2>Business Information</h2>
|
||||
<dl class="row">
|
||||
<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>
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
|
@ -52,7 +52,7 @@
|
||||
{
|
||||
<tr>
|
||||
<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>@provider.Type.GetDisplayAttribute()?.GetShortName()</td>
|
||||
<td>@provider.Status</td>
|
||||
|
@ -45,7 +45,7 @@
|
||||
{
|
||||
<tr>
|
||||
<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>
|
||||
@providerOrganization.Status
|
||||
|
@ -1,9 +1,9 @@
|
||||
@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>
|
||||
@await Html.PartialAsync("_ViewInformation", Model)
|
||||
|
@ -28,7 +28,7 @@
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<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>
|
||||
@ -68,7 +68,7 @@
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<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>
|
||||
|
@ -87,6 +87,7 @@ public static class RolePermissionMapping
|
||||
Permission.Provider_List_View,
|
||||
Permission.Provider_Create,
|
||||
Permission.Provider_View,
|
||||
Permission.Provider_Edit,
|
||||
Permission.Provider_ResendEmailInvite,
|
||||
Permission.Tools_ChargeBrainTreeCustomer,
|
||||
Permission.Tools_PromoteAdmin,
|
||||
|
@ -4,6 +4,7 @@ using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.OrganizationUsers;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
@ -41,6 +42,7 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public OrganizationUsersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -56,7 +58,8 @@ public class OrganizationUsersController : Controller
|
||||
IUpdateOrganizationUserGroupsCommand updateOrganizationUserGroupsCommand,
|
||||
IAcceptOrgUserCommand acceptOrgUserCommand,
|
||||
IAuthorizationService authorizationService,
|
||||
IApplicationCacheService applicationCacheService)
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -72,6 +75,7 @@ public class OrganizationUsersController : Controller
|
||||
_acceptOrgUserCommand = acceptOrgUserCommand;
|
||||
_authorizationService = authorizationService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -305,43 +309,34 @@ public class OrganizationUsersController : Controller
|
||||
|
||||
[HttpPut("{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(orgGuidId))
|
||||
if (!await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
|
||||
if (organizationUser == null || organizationUser.OrganizationId != orgGuidId)
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(id);
|
||||
if (organizationUser == null || organizationUser.OrganizationId != orgId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId.Value,
|
||||
model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), model.Groups);
|
||||
}
|
||||
// If admins are not allowed access to all collections, you cannot add yourself to a group
|
||||
// In this case we just don't update 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")]
|
||||
[HttpPost("{id}/groups")]
|
||||
public async Task PutGroups(string orgId, string id, [FromBody] OrganizationUserUpdateGroupsRequestModel model)
|
||||
{
|
||||
var orgGuidId = new Guid(orgId);
|
||||
if (!await _currentContext.ManageUsers(orgGuidId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var groups = restrictEditingGroups
|
||||
? null
|
||||
: model.Groups;
|
||||
|
||||
var organizationUser = await _organizationUserRepository.GetByIdAsync(new Guid(id));
|
||||
if (organizationUser == null || organizationUser.OrganizationId != orgGuidId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var loggedInUserId = _userService.GetProperUserId(User);
|
||||
await _updateOrganizationUserGroupsCommand.UpdateUserGroupsAsync(organizationUser, model.GroupIds.Select(g => new Guid(g)), loggedInUserId);
|
||||
await _organizationService.SaveUserAsync(model.ToOrganizationUser(organizationUser), userId,
|
||||
model.Collections?.Select(c => c.ToSelectionReadOnly()).ToList(), groups);
|
||||
}
|
||||
|
||||
[HttpPut("{userId}/reset-password-enrollment")]
|
||||
|
@ -261,19 +261,19 @@ public class OrganizationsController : Controller
|
||||
return new OrganizationAutoEnrollStatusResponseModel(organization.Id, data?.AutoEnrollEnabled ?? false);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/risks-subscription-failure")]
|
||||
public async Task<OrganizationRisksSubscriptionFailureResponseModel> RisksSubscriptionFailure(Guid id)
|
||||
[HttpGet("{id}/billing-status")]
|
||||
public async Task<OrganizationBillingStatusResponseModel> GetBillingStatus(Guid id)
|
||||
{
|
||||
if (!await _currentContext.EditPaymentMethods(id))
|
||||
{
|
||||
return new OrganizationRisksSubscriptionFailureResponseModel(id, false);
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
|
||||
var risksSubscriptionFailure = await _paymentService.RisksSubscriptionFailure(organization);
|
||||
|
||||
return new OrganizationRisksSubscriptionFailureResponseModel(id, risksSubscriptionFailure);
|
||||
return new OrganizationBillingStatusResponseModel(organization, risksSubscriptionFailure);
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
@ -303,7 +303,7 @@ public class OrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.BusinessName ||
|
||||
var updateBilling = !_globalSettings.SelfHosted && (model.BusinessName != organization.DisplayBusinessName() ||
|
||||
model.BillingEmail != organization.BillingEmail);
|
||||
|
||||
var hasRequiredPermissions = updateBilling
|
||||
@ -464,8 +464,8 @@ public class OrganizationsController : Controller
|
||||
await _organizationService.VerifyBankAsync(orgIdGuid, model.Amount1.Value, model.Amount2.Value);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/churn")]
|
||||
public async Task PostChurn(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
|
||||
[HttpPost("{id}/cancel")]
|
||||
public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
|
||||
{
|
||||
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")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostReinstate(string id)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
@ -9,9 +10,11 @@ namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
public class OrganizationCreateRequestModel : IValidatableObject
|
||||
{
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
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; }
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
|
@ -1,16 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
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; }
|
||||
[EmailAddress]
|
||||
[Required]
|
||||
|
@ -102,12 +102,6 @@ public class OrganizationUserUpdateRequestModel
|
||||
}
|
||||
}
|
||||
|
||||
public class OrganizationUserUpdateGroupsRequestModel
|
||||
{
|
||||
[Required]
|
||||
public IEnumerable<string> GroupIds { get; set; }
|
||||
}
|
||||
|
||||
public class OrganizationUserResetPasswordEnrollmentRequestModel
|
||||
{
|
||||
public string ResetPasswordKey { get; set; }
|
||||
|
@ -1,14 +1,18 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Providers;
|
||||
|
||||
public class ProviderSetupRequestModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
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; }
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
|
@ -1,15 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Providers;
|
||||
|
||||
public class ProviderUpdateRequestModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
[StringLength(50, ErrorMessage = "The field Name exceeds the maximum length.")]
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
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; }
|
||||
[EmailAddress]
|
||||
[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.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
@ -60,7 +61,9 @@ public class OrganizationResponseModel : ResponseModel
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string BusinessName { get; set; }
|
||||
public string BusinessAddress1 { 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.Models.Data;
|
||||
using Bit.Core.Enums;
|
||||
@ -103,6 +104,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
@ -135,6 +137,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public Guid? UserId { get; set; }
|
||||
public bool HasPublicAndPrivateKeys { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public ProviderType? ProviderType { 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.Models.Api;
|
||||
using Bit.Core.Models.Data;
|
||||
@ -23,6 +24,7 @@ public class ProfileProviderResponseModel : ResponseModel
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public string Key { 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.Models.Api;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Providers;
|
||||
|
||||
@ -68,5 +70,6 @@ public class ProviderOrganizationOrganizationDetailsResponseModel : ProviderOrga
|
||||
OrganizationName = providerOrganization.OrganizationName;
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
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.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Providers;
|
||||
|
||||
@ -25,6 +27,7 @@ public class ProviderResponseModel : ResponseModel
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public string BusinessName { get; set; }
|
||||
public string BusinessAddress1 { get; set; }
|
||||
|
@ -32,15 +32,10 @@
|
||||
</Choose>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.AzureServiceBus" Version="6.1.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.AzureStorage" Version="6.1.2" />
|
||||
<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="AspNetCore.HealthChecks.SqlServer" Version="8.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.0" />
|
||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.10.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
|
@ -821,8 +821,8 @@ public class AccountsController : Controller
|
||||
await _userService.UpdateLicenseAsync(user, license);
|
||||
}
|
||||
|
||||
[HttpPost("churn-premium")]
|
||||
public async Task PostChurn([FromBody] SubscriptionCancellationRequestModel request)
|
||||
[HttpPost("cancel")]
|
||||
public async Task PostCancel([FromBody] SubscriptionCancellationRequestModel request)
|
||||
{
|
||||
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")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostReinstate()
|
||||
|
@ -132,7 +132,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ public class SelfHostedSponsorshipSyncJob : BaseJob
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
newTags.Add($"Org: {planName}");
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System.Text;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
@ -20,7 +19,6 @@ public class PayPalController : Controller
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly IPayPalIPNClient _payPalIPNClient;
|
||||
private readonly ITransactionRepository _transactionRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
@ -30,7 +28,6 @@ public class PayPalController : Controller
|
||||
IMailService mailService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPaymentService paymentService,
|
||||
IPayPalIPNClient payPalIPNClient,
|
||||
ITransactionRepository transactionRepository,
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
@ -39,7 +36,6 @@ public class PayPalController : Controller
|
||||
_mailService = mailService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_paymentService = paymentService;
|
||||
_payPalIPNClient = payPalIPNClient;
|
||||
_transactionRepository = transactionRepository;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
@ -91,14 +87,6 @@ public class PayPalController : Controller
|
||||
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" &&
|
||||
transactionModel.TransactionType != "merch_pmt" &&
|
||||
transactionModel.PaymentStatus != "Refunded")
|
||||
@ -204,8 +192,8 @@ public class PayPalController : Controller
|
||||
|
||||
if (parentTransaction == null)
|
||||
{
|
||||
_logger.LogError("PayPal IPN ({Id}): Could not find parent transaction", transactionModel.TransactionId);
|
||||
return BadRequest();
|
||||
_logger.LogWarning("PayPal IPN ({Id}): Could not find parent transaction", transactionModel.TransactionId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
var refundAmount = Math.Abs(transactionModel.MerchantGross);
|
||||
|
@ -3,6 +3,7 @@ using Bit.Billing.Models;
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
@ -188,7 +189,7 @@ public class StripeController : Controller
|
||||
}
|
||||
|
||||
var user = await _userService.GetUserByIdAsync(userId);
|
||||
if (user.Premium)
|
||||
if (user?.Premium == true)
|
||||
{
|
||||
await _userService.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd);
|
||||
}
|
||||
@ -250,21 +251,21 @@ public class StripeController : Controller
|
||||
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
||||
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 &&
|
||||
!string.IsNullOrEmpty(customer.Address?.PostalCode) &&
|
||||
!string.IsNullOrEmpty(customer.Address?.Country))
|
||||
customer.Tax?.AutomaticTax == StripeCustomerAutomaticTaxStatus.Supported)
|
||||
{
|
||||
subscription = await _stripeFacade.UpdateSubscription(subscription.Id,
|
||||
new SubscriptionUpdateOptions
|
||||
{
|
||||
DefaultTaxRates = new List<string>(),
|
||||
DefaultTaxRates = [],
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var updatedSubscription = pm5766AutomaticTaxIsEnabled
|
||||
? subscription
|
||||
: await VerifyCorrectTaxRateForCharge(invoice, subscription);
|
||||
@ -319,7 +320,7 @@ public class StripeController : Controller
|
||||
{
|
||||
var user = await _userService.GetUserByIdAsync(userId.Value);
|
||||
|
||||
if (user.Premium)
|
||||
if (user?.Premium == true)
|
||||
{
|
||||
await SendEmails(new List<string> { user.Email });
|
||||
}
|
||||
@ -571,7 +572,7 @@ public class StripeController : Controller
|
||||
else if (parsedEvent.Type.Equals(HandledStripeWebhook.CustomerUpdated))
|
||||
{
|
||||
var customer =
|
||||
await _stripeEventService.GetCustomer(parsedEvent, true, new List<string> { "subscriptions" });
|
||||
await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]);
|
||||
|
||||
if (customer.Subscriptions == null || !customer.Subscriptions.Any())
|
||||
{
|
||||
@ -614,7 +615,7 @@ public class StripeController : Controller
|
||||
{
|
||||
Customer = paymentMethod.CustomerId,
|
||||
Status = StripeSubscriptionStatus.Unpaid,
|
||||
Expand = new List<string> { "data.latest_invoice" }
|
||||
Expand = ["data.latest_invoice"]
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
@ -682,29 +683,35 @@ public class StripeController : Controller
|
||||
Guid? orgId = 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");
|
||||
if (!string.IsNullOrWhiteSpace(orgIdKey))
|
||||
return new Tuple<Guid?, Guid?>(orgId, userId);
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
else
|
||||
{
|
||||
var userIdKey = metaData.Keys.FirstOrDefault(k => k.ToLowerInvariant() == "userid");
|
||||
if (!string.IsNullOrWhiteSpace(userIdKey))
|
||||
{
|
||||
userId = new Guid(metaData[userIdKey]);
|
||||
}
|
||||
userId = new Guid(metaData[userIdKey]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -891,9 +898,9 @@ public class StripeController : Controller
|
||||
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);
|
||||
|
||||
return subscription;
|
||||
|
@ -25,7 +25,10 @@ public class PayPalIPNTransactionModel
|
||||
|
||||
var data = queryString
|
||||
.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");
|
||||
TransactionType = Extract(data, "txn_type");
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
@ -17,8 +18,14 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
public Guid Id { get; set; }
|
||||
[MaxLength(50)]
|
||||
public string Identifier { get; set; }
|
||||
/// <summary>
|
||||
/// This value is HTML encoded. For display purposes use the method DisplayName() instead.
|
||||
/// </summary>
|
||||
[MaxLength(50)]
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// This value is HTML encoded. For display purposes use the method DisplayBusinessName() instead.
|
||||
/// </summary>
|
||||
[MaxLength(50)]
|
||||
public string BusinessName { get; set; }
|
||||
[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()
|
||||
{
|
||||
return BillingEmail?.ToLowerInvariant()?.Trim();
|
||||
@ -111,12 +134,12 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
|
||||
public string BillingName()
|
||||
{
|
||||
return BusinessName;
|
||||
return DisplayBusinessName();
|
||||
}
|
||||
|
||||
public string SubscriberName()
|
||||
{
|
||||
return Name;
|
||||
return DisplayName();
|
||||
}
|
||||
|
||||
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.Enums;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Entities.Provider;
|
||||
@ -7,7 +9,13 @@ namespace Bit.Core.AdminConsole.Entities.Provider;
|
||||
public class Provider : ITableObject<Guid>
|
||||
{
|
||||
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; }
|
||||
/// <summary>
|
||||
/// This value is HTML encoded. For display purposes use the method DisplayBusinessName() instead.
|
||||
/// </summary>
|
||||
public string BusinessName { get; set; }
|
||||
public string BusinessAddress1 { get; set; }
|
||||
public string BusinessAddress2 { get; set; }
|
||||
@ -22,6 +30,9 @@ public class Provider : ITableObject<Guid>
|
||||
public bool Enabled { get; set; } = true;
|
||||
public DateTime CreationDate { 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()
|
||||
{
|
||||
@ -30,4 +41,20 @@ public class Provider : ITableObject<Guid>
|
||||
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;
|
||||
|
||||
@ -6,6 +8,7 @@ public class OrganizationUserOrganizationDetails
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
@ -37,6 +40,7 @@ public class OrganizationUserOrganizationDetails
|
||||
public string PublicKey { get; set; }
|
||||
public string PrivateKey { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public ProviderType? ProviderType { 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;
|
||||
|
||||
@ -7,6 +10,10 @@ public class ProviderOrganizationOrganizationDetails
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProviderId { 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 Key { get; set; }
|
||||
public string Settings { get; set; }
|
||||
@ -16,4 +23,12 @@ public class ProviderOrganizationOrganizationDetails
|
||||
public int? Seats { get; set; }
|
||||
public string Plan { 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;
|
||||
|
||||
@ -7,6 +9,7 @@ public class ProviderOrganizationProviderDetails
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProviderId { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { 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;
|
||||
|
||||
@ -6,6 +8,7 @@ public class ProviderUserOrganizationDetails
|
||||
{
|
||||
public Guid OrganizationId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public bool UsePolicies { get; set; }
|
||||
public bool UseSso { get; set; }
|
||||
@ -33,6 +36,7 @@ public class ProviderUserOrganizationDetails
|
||||
public string PrivateKey { get; set; }
|
||||
public Guid? ProviderId { get; set; }
|
||||
public Guid? ProviderUserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public Core.Enums.PlanType PlanType { 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;
|
||||
|
||||
@ -6,6 +8,7 @@ public class ProviderUserProviderDetails
|
||||
{
|
||||
public Guid ProviderId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public string Key { 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;
|
||||
|
||||
@ -7,6 +9,7 @@ public class ProviderUserUserDetails
|
||||
public Guid Id { get; set; }
|
||||
public Guid ProviderId { get; set; }
|
||||
public Guid? UserId { get; set; }
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string Name { get; set; }
|
||||
public string Email { get; set; }
|
||||
public ProviderUserStatusType Status { get; set; }
|
||||
|
@ -537,6 +537,7 @@ public class OrganizationService : IOrganizationService
|
||||
Storage = returnValue.Item1.MaxStorageGb,
|
||||
// TODO: add reference events for SmSeats and Service Accounts - see AC-1481
|
||||
});
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
@ -819,7 +820,7 @@ public class OrganizationService : IOrganizationService
|
||||
await customerService.UpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
Email = organization.BillingEmail,
|
||||
Description = organization.BusinessName
|
||||
Description = organization.DisplayBusinessName()
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1276,7 +1277,7 @@ public class OrganizationService : IOrganizationService
|
||||
orgUser.Email = null;
|
||||
|
||||
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);
|
||||
succeededUsers.Add(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
|
||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(user.OrganizationId);
|
||||
if (organizationAbility?.FlexibleCollections == true && user.Type == OrganizationUserType.Manager)
|
||||
var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId);
|
||||
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.");
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
if (organizationAbility?.FlexibleCollections == true && collections?.Any() == true)
|
||||
if (organization.FlexibleCollections && collections?.Any() == true)
|
||||
{
|
||||
var invalidAssociations = collections.Where(cas => cas.Manage && (cas.ReadOnly || cas.HidePasswords));
|
||||
if (invalidAssociations.Any())
|
||||
@ -1440,7 +1441,6 @@ public class OrganizationService : IOrganizationService
|
||||
var additionalSmSeatsRequired = await _countNewSmSeatsRequiredQuery.CountNewSmSeatsRequiredAsync(user.OrganizationId, 1);
|
||||
if (additionalSmSeatsRequired > 0)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(user.OrganizationId);
|
||||
var update = new SecretsManagerSubscriptionUpdate(organization, true)
|
||||
.AdjustSeats(additionalSmSeatsRequired);
|
||||
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(update);
|
||||
|
@ -124,14 +124,21 @@ public class PolicyService : IPolicyService
|
||||
switch (policy.Type)
|
||||
{
|
||||
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 (!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,
|
||||
savingUserId);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||
org.Name, orgUser.Email);
|
||||
org.DisplayName(), orgUser.Email);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -147,7 +154,7 @@ public class PolicyService : IPolicyService
|
||||
await organizationService.DeleteUserAsync(policy.OrganizationId, orgUser.Id,
|
||||
savingUserId);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
|
||||
org.Name, orgUser.Email);
|
||||
org.DisplayName(), orgUser.Email);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -4,6 +4,7 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Duo = DuoUniversal;
|
||||
|
||||
namespace Bit.Core.Auth.Identity;
|
||||
@ -25,6 +26,7 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IDataProtectorTokenFactory<DuoUserStateTokenable> _tokenDataFactory;
|
||||
private readonly ILogger<TemporaryDuoWebV4SDKService> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the DuoUniversalPromptService. Used to supplement v2 implementation of Duo with v4 SDK
|
||||
@ -34,11 +36,13 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
|
||||
public TemporaryDuoWebV4SDKService(
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory)
|
||||
IDataProtectorTokenFactory<DuoUserStateTokenable> tokenDataFactory,
|
||||
ILogger<TemporaryDuoWebV4SDKService> logger)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_globalSettings = globalSettings;
|
||||
_tokenDataFactory = tokenDataFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -129,8 +133,9 @@ public class TemporaryDuoWebV4SDKService : ITemporaryDuoWebV4SDKService
|
||||
(string)provider.MetaData["Host"],
|
||||
redirectUri).Build();
|
||||
|
||||
if (!await client.DoHealthCheck())
|
||||
if (!await client.DoHealthCheck(true))
|
||||
{
|
||||
_logger.LogError("Unable to connect to Duo. Health check failed.");
|
||||
return null;
|
||||
}
|
||||
return client;
|
||||
|
@ -103,19 +103,27 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider<User>
|
||||
// established ownership in this context.
|
||||
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
|
||||
webAuthCred.Item2.SignatureCounter = res.Counter;
|
||||
// Update SignatureCounter
|
||||
webAuthCred.Item2.SignatureCounter = res.Counter;
|
||||
|
||||
var providers = user.GetTwoFactorProviders();
|
||||
providers[TwoFactorProviderType.WebAuthn].MetaData[webAuthCred.Item1] = webAuthCred.Item2;
|
||||
user.SetTwoFactorProviders(providers);
|
||||
await userService.UpdateTwoFactorProviderAsync(user, TwoFactorProviderType.WebAuthn, logEvent: false);
|
||||
var providers = user.GetTwoFactorProviders();
|
||||
providers[TwoFactorProviderType.WebAuthn].MetaData[webAuthCred.Item1] = webAuthCred.Item2;
|
||||
user.SetTwoFactorProviders(providers);
|
||||
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)
|
||||
|
@ -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 Fido2VaultCredentials = "fido2-vault-credentials";
|
||||
public const string VaultOnboarding = "vault-onboarding";
|
||||
public const string AutofillV2 = "autofill-v2";
|
||||
public const string BrowserFilelessImport = "browser-fileless-import";
|
||||
/// <summary>
|
||||
/// 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 FlexibleCollectionsV1 = "flexible-collections-v-1"; // v-1 is intentional
|
||||
public const string BulkCollectionAccess = "bulk-collection-access";
|
||||
public const string AutofillOverlay = "autofill-overlay";
|
||||
public const string ItemShare = "item-share";
|
||||
public const string KeyRotationImprovements = "key-rotation-improvements";
|
||||
public const string DuoRedirect = "duo-redirect";
|
||||
@ -129,9 +127,10 @@ public static class FeatureFlagKeys
|
||||
/// flexible collections
|
||||
/// </summary>
|
||||
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 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()
|
||||
{
|
||||
|
@ -21,8 +21,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.52" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.52" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.59" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.59" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.2" />
|
||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.15.0" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
|
||||
@ -32,22 +32,22 @@
|
||||
<PackageReference Include="DnsClient" Version="1.7.0" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||
<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.Azure.Cosmos" Version="3.38.0" />
|
||||
<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.Extensions.Caching.Cosmos" Version="1.6.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.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="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.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="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="3.0.1" />
|
||||
@ -57,7 +57,7 @@
|
||||
<PackageReference Include="Otp.NET" Version="1.3.0" />
|
||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||
<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>
|
||||
|
@ -20,6 +20,7 @@ public class Transaction : ITableObject<Guid>
|
||||
[MaxLength(50)]
|
||||
public string GatewayId { get; set; }
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
public Guid? ProviderId { get; set; }
|
||||
|
||||
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));
|
||||
}
|
||||
CollectionMethod = sub.CollectionMethod;
|
||||
}
|
||||
|
||||
public DateTime? TrialStartDate { get; set; }
|
||||
@ -54,6 +55,7 @@ public class SubscriptionInfo
|
||||
public string Status { get; set; }
|
||||
public bool Cancelled { get; set; }
|
||||
public IEnumerable<BillingSubscriptionItem> Items { get; set; } = new List<BillingSubscriptionItem>();
|
||||
public string CollectionMethod { get; set; }
|
||||
|
||||
public class BillingSubscriptionItem
|
||||
{
|
||||
|
@ -15,7 +15,7 @@ public class OrganizationInvitesInfo
|
||||
bool initOrganization = false
|
||||
)
|
||||
{
|
||||
OrganizationName = org.Name;
|
||||
OrganizationName = org.DisplayName();
|
||||
OrgSsoIdentifier = org.Identifier;
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
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>> GetManyByOrganizationIdAsync(Guid organizationId);
|
||||
Task<ICollection<Transaction>> GetManyByProviderIdAsync(Guid providerId);
|
||||
Task<Transaction> GetByGatewayIdAsync(GatewayType gatewayType, string gatewayId);
|
||||
}
|
||||
|
@ -77,5 +77,6 @@ public interface IMailService
|
||||
Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(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 SendTrialInitiationEmailAsync(string email);
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ public class HandlebarsMailService : IMailService
|
||||
|
||||
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
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
@ -160,7 +160,7 @@ public class HandlebarsMailService : IMailService
|
||||
|
||||
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
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
@ -179,7 +179,7 @@ public class HandlebarsMailService : IMailService
|
||||
var model = new OrganizationUserAcceptedViewModel
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(organization.Name, false),
|
||||
OrganizationName = CoreHelpers.SanitizeForEmail(organization.DisplayName(), false),
|
||||
UserIdentifier = userIdentifier,
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName
|
||||
@ -251,6 +251,19 @@ public class HandlebarsMailService : IMailService
|
||||
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)
|
||||
{
|
||||
var message = CreateDefaultMessage("[Admin] Continue Logging In", email);
|
||||
@ -920,7 +933,7 @@ public class HandlebarsMailService : IMailService
|
||||
public async Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
||||
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
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
@ -935,7 +948,7 @@ public class HandlebarsMailService : IMailService
|
||||
public async Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount,
|
||||
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
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
|
@ -132,13 +132,13 @@ public class LicensingService : ILicensingService
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, null,
|
||||
"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.ExpirationDate = license?.Expires ?? DateTime.UtcNow;
|
||||
org.RevisionDate = DateTime.UtcNow;
|
||||
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()
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -125,59 +126,61 @@ public class StripePaymentService : IPaymentService
|
||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
|
||||
, additionalSmSeats, additionalServiceAccount);
|
||||
|
||||
Stripe.Customer customer = null;
|
||||
Stripe.Subscription subscription;
|
||||
Customer customer = null;
|
||||
Subscription subscription;
|
||||
try
|
||||
{
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Description = org.BusinessName,
|
||||
Description = org.DisplayBusinessName(),
|
||||
Email = org.BillingEmail,
|
||||
Source = stipeCustomerSourceToken,
|
||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||
Metadata = stripeCustomerMetadata,
|
||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
||||
{
|
||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = org.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
Coupon = signupIsFromSecretsManagerTrial
|
||||
? SecretsManagerStandaloneDiscountId
|
||||
: provider
|
||||
? ProviderDiscountId
|
||||
: null,
|
||||
Address = new Stripe.AddressOptions
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
Country = taxInfo?.BillingAddressCountry,
|
||||
PostalCode = taxInfo?.BillingAddressPostalCode,
|
||||
// Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead.
|
||||
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
|
||||
Line2 = taxInfo.BillingAddressLine2,
|
||||
City = taxInfo.BillingAddressCity,
|
||||
State = taxInfo.BillingAddressState,
|
||||
Line1 = taxInfo?.BillingAddressLine1 ?? string.Empty,
|
||||
Line2 = taxInfo?.BillingAddressLine2,
|
||||
City = taxInfo?.BillingAddressCity,
|
||||
State = taxInfo?.BillingAddressState,
|
||||
},
|
||||
TaxIdData = !taxInfo.HasTaxId ? null : new List<Stripe.CustomerTaxIdDataOptions>
|
||||
{
|
||||
new Stripe.CustomerTaxIdDataOptions
|
||||
{
|
||||
Type = taxInfo.TaxIdType,
|
||||
Value = taxInfo.TaxIdNumber,
|
||||
},
|
||||
},
|
||||
});
|
||||
TaxIdData = taxInfo?.HasTaxId != true
|
||||
? null
|
||||
:
|
||||
[
|
||||
new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber, }
|
||||
],
|
||||
};
|
||||
|
||||
customerCreateOptions.AddExpand("tax");
|
||||
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||
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);
|
||||
@ -185,7 +188,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@ -252,9 +255,10 @@ public class StripePaymentService : IPaymentService
|
||||
throw new BadRequestException("Organization already has a subscription.");
|
||||
}
|
||||
|
||||
var customerOptions = new Stripe.CustomerGetOptions();
|
||||
var customerOptions = new CustomerGetOptions();
|
||||
customerOptions.AddExpand("default_source");
|
||||
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
||||
customerOptions.AddExpand("tax");
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions);
|
||||
if (customer == null)
|
||||
{
|
||||
@ -301,14 +305,15 @@ public class StripePaymentService : IPaymentService
|
||||
var customerUpdateOptions = new CustomerUpdateOptions { Address = addressOptions };
|
||||
customerUpdateOptions.AddExpand("default_source");
|
||||
customerUpdateOptions.AddExpand("invoice_settings.default_payment_method");
|
||||
customerUpdateOptions.AddExpand("tax");
|
||||
customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions);
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@ -333,7 +338,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
|
||||
Stripe.Customer customer, Stripe.SubscriptionCreateOptions subCreateOptions)
|
||||
Customer customer, SubscriptionCreateOptions subCreateOptions)
|
||||
{
|
||||
var stripePaymentMethod = false;
|
||||
var paymentMethodType = PaymentMethodType.Credit;
|
||||
@ -351,12 +356,12 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
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;
|
||||
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;
|
||||
stripePaymentMethod = true;
|
||||
@ -394,7 +399,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
var createdStripeCustomer = false;
|
||||
Stripe.Customer customer = null;
|
||||
Customer customer = null;
|
||||
Braintree.Customer braintreeCustomer = null;
|
||||
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount
|
||||
or PaymentMethodType.Credit;
|
||||
@ -422,14 +427,23 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
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))
|
||||
{
|
||||
var stripeCustomerMetadata = new Dictionary<string, string> { { "region", _globalSettings.BaseServiceUri.CloudRegion } };
|
||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
||||
};
|
||||
if (paymentMethodType == PaymentMethodType.PayPal)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Description = user.Name,
|
||||
Email = user.Email,
|
||||
Metadata = stripeCustomerMetadata,
|
||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||
Source = stipeCustomerSourceToken,
|
||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
||||
{
|
||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = user.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(user.SubscriberName()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
Address = new Stripe.AddressOptions
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Line1 = string.Empty,
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
},
|
||||
});
|
||||
};
|
||||
customerCreateOptions.AddExpand("tax");
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
createdStripeCustomer = true;
|
||||
}
|
||||
|
||||
@ -492,17 +509,17 @@ public class StripePaymentService : IPaymentService
|
||||
throw new GatewayException("Could not set up customer payment profile.");
|
||||
}
|
||||
|
||||
var subCreateOptions = new Stripe.SubscriptionCreateOptions
|
||||
var subCreateOptions = new SubscriptionCreateOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
Items = new List<Stripe.SubscriptionItemOptions>(),
|
||||
Items = [],
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[user.GatewayIdField()] = user.Id.ToString()
|
||||
}
|
||||
};
|
||||
|
||||
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = PremiumPlanId,
|
||||
Quantity = 1
|
||||
@ -524,25 +541,22 @@ public class StripePaymentService : IPaymentService
|
||||
var taxRate = taxRates.FirstOrDefault();
|
||||
if (taxRate != null)
|
||||
{
|
||||
subCreateOptions.DefaultTaxRates = new List<string>(1)
|
||||
{
|
||||
taxRate.Id
|
||||
};
|
||||
subCreateOptions.DefaultTaxRates = [taxRate.Id];
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalStorageGb > 0)
|
||||
{
|
||||
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = StoragePlanId,
|
||||
Quantity = additionalStorageGb
|
||||
});
|
||||
}
|
||||
|
||||
if (pm5766AutomaticTaxIsEnabled)
|
||||
if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
|
||||
{
|
||||
subCreateOptions.DefaultTaxRates = new List<string>();
|
||||
subCreateOptions.DefaultTaxRates = [];
|
||||
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
|
||||
@ -558,34 +572,33 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||
}
|
||||
else
|
||||
{
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
}
|
||||
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
||||
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,
|
||||
Stripe.SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
|
||||
SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
|
||||
{
|
||||
var addedCreditToStripeCustomer = false;
|
||||
Braintree.Transaction braintreeTransaction = null;
|
||||
|
||||
var subInvoiceMetadata = new Dictionary<string, string>();
|
||||
Stripe.Subscription subscription = null;
|
||||
Subscription subscription = null;
|
||||
try
|
||||
{
|
||||
if (!stripePaymentMethod)
|
||||
{
|
||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items)
|
||||
});
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax))
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax) &&
|
||||
CustomerHasTaxLocationVerified(customer))
|
||||
{
|
||||
previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true };
|
||||
}
|
||||
@ -632,7 +645,7 @@ public class StripePaymentService : IPaymentService
|
||||
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
|
||||
});
|
||||
@ -649,10 +662,10 @@ public class StripePaymentService : IPaymentService
|
||||
};
|
||||
|
||||
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
||||
if (pm5766AutomaticTaxIsEnabled)
|
||||
if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
|
||||
{
|
||||
upcomingInvoiceOptions.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
||||
upcomingInvoiceOptions.SubscriptionDefaultTaxRates = new List<string>();
|
||||
upcomingInvoiceOptions.SubscriptionDefaultTaxRates = [];
|
||||
}
|
||||
|
||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions);
|
||||
@ -666,17 +679,12 @@ public class StripePaymentService : IPaymentService
|
||||
subCreateOptions.OffSession = true;
|
||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax))
|
||||
{
|
||||
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
|
||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
||||
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@ -694,7 +702,7 @@ public class StripePaymentService : IPaymentService
|
||||
throw new GatewayException("Invoice not found.");
|
||||
}
|
||||
|
||||
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions
|
||||
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions
|
||||
{
|
||||
Metadata = subInvoiceMetadata
|
||||
});
|
||||
@ -712,7 +720,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
else if (addedCreditToStripeCustomer || customer.Balance < 0)
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Balance = customer.Balance
|
||||
});
|
||||
@ -727,7 +735,7 @@ public class StripePaymentService : IPaymentService
|
||||
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))
|
||||
{
|
||||
throw new GatewayException("Bank account is not yet verified.");
|
||||
@ -737,10 +745,10 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
private List<Stripe.InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
||||
List<Stripe.SubscriptionItemOptions> subItemOptions)
|
||||
private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
||||
List<SubscriptionItemOptions> subItemOptions)
|
||||
{
|
||||
return subItemOptions.Select(si => new Stripe.InvoiceSubscriptionItemOptions
|
||||
return subItemOptions.Select(si => new InvoiceSubscriptionItemOptions
|
||||
{
|
||||
Plan = si.Plan,
|
||||
Price = si.Price,
|
||||
@ -753,7 +761,10 @@ public class StripePaymentService : IPaymentService
|
||||
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate, bool invoiceNow = false)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
throw new GatewayException("Subscription not found.");
|
||||
@ -766,7 +777,7 @@ public class StripePaymentService : IPaymentService
|
||||
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
||||
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
|
||||
|
||||
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions
|
||||
var subUpdateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = updatedItemOptions,
|
||||
ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow
|
||||
@ -777,7 +788,7 @@ public class StripePaymentService : IPaymentService
|
||||
ProrationDate = prorationDate,
|
||||
};
|
||||
var immediatelyInvoice = false;
|
||||
if (!invoiceNow && isPm5864DollarThresholdEnabled)
|
||||
if (!invoiceNow && isPm5864DollarThresholdEnabled && sub.Status.Trim() != "trialing")
|
||||
{
|
||||
var upcomingInvoiceWithChanges = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
|
||||
{
|
||||
@ -789,7 +800,8 @@ public class StripePaymentService : IPaymentService
|
||||
SubscriptionBillingCycleAnchor = SubscriptionBillingCycleAnchor.Now
|
||||
});
|
||||
|
||||
immediatelyInvoice = upcomingInvoiceWithChanges.AmountRemaining >= 50000;
|
||||
var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
|
||||
immediatelyInvoice = isAnnualPlan && upcomingInvoiceWithChanges.AmountRemaining >= 50000;
|
||||
|
||||
subUpdateOptions.BillingCycleAnchor = immediatelyInvoice
|
||||
? SubscriptionBillingCycleAnchor.Now
|
||||
@ -797,9 +809,11 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@ -824,7 +838,7 @@ public class StripePaymentService : IPaymentService
|
||||
var taxRate = taxRates.FirstOrDefault();
|
||||
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 invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
|
||||
var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new InvoiceGetOptions());
|
||||
if (invoice == null)
|
||||
{
|
||||
throw new BadRequestException("Unable to locate draft invoice for subscription update.");
|
||||
@ -852,11 +866,11 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
else
|
||||
{
|
||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions
|
||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new InvoiceFinalizeOptions
|
||||
{
|
||||
AutoAdvance = false,
|
||||
});
|
||||
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions());
|
||||
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new InvoiceSendOptions());
|
||||
paymentIntentClientSecret = null;
|
||||
}
|
||||
}
|
||||
@ -864,7 +878,7 @@ public class StripePaymentService : IPaymentService
|
||||
catch
|
||||
{
|
||||
// Need to revert the subscription
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = subscriptionUpdate.RevertItemsOptions(sub),
|
||||
// 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
|
||||
if (collectionMethod != "send_invoice" || daysUntilDue == null)
|
||||
{
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new SubscriptionUpdateOptions
|
||||
{
|
||||
CollectionMethod = collectionMethod,
|
||||
DaysUntilDue = daysUntilDue,
|
||||
@ -950,7 +964,7 @@ public class StripePaymentService : IPaymentService
|
||||
if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
|
||||
{
|
||||
await _stripeAdapter.SubscriptionCancelAsync(subscriber.GatewaySubscriptionId,
|
||||
new Stripe.SubscriptionCancelOptions());
|
||||
new SubscriptionCancelOptions());
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||
@ -983,7 +997,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
else
|
||||
{
|
||||
var charges = await _stripeAdapter.ChargeListAsync(new Stripe.ChargeListOptions
|
||||
var charges = await _stripeAdapter.ChargeListAsync(new ChargeListOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId
|
||||
});
|
||||
@ -992,7 +1006,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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("invoice_settings.default_payment_method");
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
|
||||
@ -1016,7 +1030,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card";
|
||||
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)
|
||||
{
|
||||
cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id;
|
||||
@ -1029,7 +1043,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions
|
||||
await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new InvoiceFinalizeOptions
|
||||
{
|
||||
AutoAdvance = false
|
||||
});
|
||||
@ -1045,11 +1059,11 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
// Finalize the invoice (from Draft) w/o auto-advance so we
|
||||
// can attempt payment manually.
|
||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions
|
||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new InvoiceFinalizeOptions
|
||||
{
|
||||
AutoAdvance = false,
|
||||
});
|
||||
var invoicePayOptions = new Stripe.InvoicePayOptions
|
||||
var invoicePayOptions = new InvoicePayOptions
|
||||
{
|
||||
PaymentMethod = cardPaymentMethodId,
|
||||
};
|
||||
@ -1083,7 +1097,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
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>
|
||||
{
|
||||
@ -1099,13 +1113,13 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
invoice = await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions);
|
||||
}
|
||||
catch (Stripe.StripeException e)
|
||||
catch (StripeException e)
|
||||
{
|
||||
if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&
|
||||
e.StripeError?.Code == "invoice_payment_intent_requires_action")
|
||||
{
|
||||
// SCA required, get intent client secret
|
||||
var invoiceGetOptions = new Stripe.InvoiceGetOptions();
|
||||
var invoiceGetOptions = new InvoiceGetOptions();
|
||||
invoiceGetOptions.AddExpand("payment_intent");
|
||||
invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions);
|
||||
paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret;
|
||||
@ -1130,7 +1144,7 @@ public class StripePaymentService : IPaymentService
|
||||
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
|
||||
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.
|
||||
if (customer.Balance == 0)
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
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))
|
||||
{
|
||||
throw new GatewayException("Bank account is not yet verified.");
|
||||
@ -1192,14 +1206,14 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
var canceledSub = endOfPeriod ?
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
|
||||
new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) :
|
||||
await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new Stripe.SubscriptionCancelOptions());
|
||||
new SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) :
|
||||
await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new SubscriptionCancelOptions());
|
||||
if (!canceledSub.CanceledAt.HasValue)
|
||||
{
|
||||
throw new GatewayException("Unable to cancel subscription.");
|
||||
}
|
||||
}
|
||||
catch (Stripe.StripeException e)
|
||||
catch (StripeException e)
|
||||
{
|
||||
if (e.Message != $"No such subscription: {subscriber.GatewaySubscriptionId}")
|
||||
{
|
||||
@ -1233,7 +1247,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
var updatedSub = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
|
||||
new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
|
||||
new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
|
||||
if (updatedSub.CanceledAt.HasValue)
|
||||
{
|
||||
throw new GatewayException("Unable to reinstate subscription.");
|
||||
@ -1264,12 +1278,11 @@ public class StripePaymentService : IPaymentService
|
||||
};
|
||||
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount;
|
||||
|
||||
Stripe.Customer customer = null;
|
||||
Customer customer = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||
{
|
||||
var options = new Stripe.CustomerGetOptions();
|
||||
options.AddExpand("sources");
|
||||
var options = new CustomerGetOptions { Expand = ["sources", "tax", "subscriptions"] };
|
||||
customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, options);
|
||||
if (customer.Metadata?.Any() ?? false)
|
||||
{
|
||||
@ -1369,26 +1382,27 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
if (customer == null)
|
||||
{
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions
|
||||
{
|
||||
Description = subscriber.BillingName(),
|
||||
Email = subscriber.BillingEmailAddress(),
|
||||
Metadata = stripeCustomerMetadata,
|
||||
Source = stipeCustomerSourceToken,
|
||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
||||
{
|
||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = subscriber.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
Address = taxInfo == null ? null : new Stripe.AddressOptions
|
||||
Address = taxInfo == null ? null : new AddressOptions
|
||||
{
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
@ -1397,7 +1411,7 @@ public class StripePaymentService : IPaymentService
|
||||
City = taxInfo.BillingAddressCity,
|
||||
State = taxInfo.BillingAddressState,
|
||||
},
|
||||
Expand = new List<string> { "sources" },
|
||||
Expand = ["sources", "tax", "subscriptions"],
|
||||
});
|
||||
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
@ -1413,7 +1427,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
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
|
||||
});
|
||||
@ -1422,7 +1436,7 @@ public class StripePaymentService : IPaymentService
|
||||
else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId))
|
||||
{
|
||||
await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId,
|
||||
new Stripe.PaymentMethodAttachOptions { Customer = customer.Id });
|
||||
new PaymentMethodAttachOptions { Customer = customer.Id });
|
||||
defaultPaymentMethodId = stipeCustomerPaymentMethodId;
|
||||
}
|
||||
}
|
||||
@ -1431,44 +1445,44 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
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);
|
||||
}
|
||||
else if (source is Stripe.Card)
|
||||
else if (source is Card)
|
||||
{
|
||||
await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new Stripe.PaymentMethodListOptions
|
||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new PaymentMethodListOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
Type = "card"
|
||||
});
|
||||
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,
|
||||
DefaultSource = defaultSourceId,
|
||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = defaultPaymentMethodId,
|
||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
||||
{
|
||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = subscriber.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName())
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
Address = taxInfo == null ? null : new Stripe.AddressOptions
|
||||
Address = taxInfo == null ? null : new AddressOptions
|
||||
{
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
@ -1477,8 +1491,27 @@ public class StripePaymentService : IPaymentService
|
||||
City = taxInfo.BillingAddressCity,
|
||||
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
|
||||
{
|
||||
@ -1494,7 +1527,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount)
|
||||
{
|
||||
Stripe.Customer customer = null;
|
||||
Customer customer = null;
|
||||
var customerExists = subscriber.Gateway == GatewayType.Stripe &&
|
||||
!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId);
|
||||
if (customerExists)
|
||||
@ -1503,7 +1536,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
else
|
||||
{
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions
|
||||
{
|
||||
Email = subscriber.BillingEmailAddress(),
|
||||
Description = subscriber.BillingName(),
|
||||
@ -1511,7 +1544,7 @@ public class StripePaymentService : IPaymentService
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
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)
|
||||
});
|
||||
@ -1614,7 +1647,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId,
|
||||
new Stripe.CustomerGetOptions { Expand = new List<string> { "tax_ids" } });
|
||||
new CustomerGetOptions { Expand = ["tax_ids"] });
|
||||
|
||||
if (customer == null)
|
||||
{
|
||||
@ -1647,9 +1680,9 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
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,
|
||||
Line2 = taxInfo.BillingAddressLine2,
|
||||
@ -1658,7 +1691,7 @@ public class StripePaymentService : IPaymentService
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
},
|
||||
Expand = new List<string> { "tax_ids" }
|
||||
Expand = ["tax_ids"]
|
||||
});
|
||||
|
||||
if (!subscriber.IsUser() && customer != null)
|
||||
@ -1672,7 +1705,7 @@ public class StripePaymentService : IPaymentService
|
||||
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) &&
|
||||
!string.IsNullOrWhiteSpace(taxInfo.TaxIdType))
|
||||
{
|
||||
await _stripeAdapter.TaxIdCreateAsync(customer.Id, new Stripe.TaxIdCreateOptions
|
||||
await _stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions
|
||||
{
|
||||
Type = taxInfo.TaxIdType,
|
||||
Value = taxInfo.TaxIdNumber,
|
||||
@ -1684,7 +1717,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
public async Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate)
|
||||
{
|
||||
var stripeTaxRateOptions = new Stripe.TaxRateCreateOptions()
|
||||
var stripeTaxRateOptions = new TaxRateCreateOptions()
|
||||
{
|
||||
DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}",
|
||||
Inclusive = false,
|
||||
@ -1717,7 +1750,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
var updatedStripeTaxRate = await _stripeAdapter.TaxRateUpdateAsync(
|
||||
taxRate.Id,
|
||||
new Stripe.TaxRateUpdateOptions() { Active = false }
|
||||
new TaxRateUpdateOptions() { Active = false }
|
||||
);
|
||||
if (!updatedStripeTaxRate.Active)
|
||||
{
|
||||
@ -1738,32 +1771,36 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
var subscriptionInfo = await GetSubscriptionAsync(organization);
|
||||
|
||||
if (subscriptionInfo.Subscription is not { Status: "active" or "trialing" or "past_due" } ||
|
||||
subscriptionInfo.UpcomingInvoice == null)
|
||||
if (subscriptionInfo.Subscription is not
|
||||
{
|
||||
Status: "active" or "trialing" or "past_due",
|
||||
CollectionMethod: "charge_automatically"
|
||||
}
|
||||
|| subscriptionInfo.UpcomingInvoice == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var customer = await GetCustomerAsync(organization.GatewayCustomerId);
|
||||
var customer = await GetCustomerAsync(organization.GatewayCustomerId, GetCustomerPaymentOptions());
|
||||
|
||||
var paymentSource = await GetBillingPaymentSourceAsync(customer);
|
||||
|
||||
return paymentSource == null;
|
||||
}
|
||||
|
||||
private Stripe.PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
||||
private PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
||||
{
|
||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
|
||||
new Stripe.PaymentMethodListOptions { Customer = customerId, Type = "card" });
|
||||
new PaymentMethodListOptions { Customer = customerId, Type = "card" });
|
||||
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;
|
||||
}
|
||||
|
||||
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Stripe.Customer customer)
|
||||
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Customer customer)
|
||||
{
|
||||
if (customer == null)
|
||||
{
|
||||
@ -1792,7 +1829,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@ -1801,27 +1838,27 @@ public class StripePaymentService : IPaymentService
|
||||
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("invoice_settings.default_payment_method");
|
||||
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))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Stripe.Customer customer = null;
|
||||
Customer customer = null;
|
||||
try
|
||||
{
|
||||
customer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId, options);
|
||||
}
|
||||
catch (Stripe.StripeException) { }
|
||||
catch (StripeException) { }
|
||||
|
||||
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)
|
||||
{
|
||||
@ -1865,28 +1902,32 @@ public class StripePaymentService : IPaymentService
|
||||
.OrderByDescending(invoice => invoice.Created)
|
||||
.Select(invoice => new BillingInfo.BillingInvoice(invoice));
|
||||
}
|
||||
catch (Stripe.StripeException exception)
|
||||
catch (StripeException exception)
|
||||
{
|
||||
_logger.LogError(exception, "An error occurred while listing Stripe invoices");
|
||||
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
|
||||
// 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))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
else if (subscriberName.Length <= 30)
|
||||
{
|
||||
return subscriberName;
|
||||
}
|
||||
else
|
||||
{
|
||||
return subscriberName.Substring(0, 30);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
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);
|
||||
if (result == IdentityResult.Success)
|
||||
{
|
||||
await _mailService.SendWelcomeEmailAsync(user);
|
||||
|
||||
if (!string.IsNullOrEmpty(user.ReferenceData))
|
||||
{
|
||||
var referenceData = JsonConvert.DeserializeObject<Dictionary<string, object>>(user.ReferenceData);
|
||||
if (referenceData.TryGetValue("initiationPath", out var value))
|
||||
{
|
||||
var initiationPath = value.ToString();
|
||||
await SendAppropriateWelcomeEmailAsync(user, initiationPath);
|
||||
if (!string.IsNullOrEmpty(initiationPath))
|
||||
{
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
@ -797,7 +796,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
user.ForcePasswordReset = true;
|
||||
|
||||
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 _pushService.PushLogOutAsync(user.Id);
|
||||
|
||||
@ -1391,7 +1390,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
await organizationService.DeleteUserAsync(p.OrganizationId, user.Id);
|
||||
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
|
||||
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
|
||||
organization.Name, user.Email);
|
||||
organization.DisplayName(), user.Email);
|
||||
}).ToArray();
|
||||
|
||||
await Task.WhenAll(removeOrgUserTasks);
|
||||
@ -1453,4 +1452,18 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public Task SendTrialInitiationEmailAsync(string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,6 +117,11 @@ public class Send : ITableObject<Guid>
|
||||
/// </value>
|
||||
public bool? HideEmail { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Identifies the Cipher associated with this send.
|
||||
/// </summary>
|
||||
public Guid? CipherId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Generates the send's <see cref="Id" />
|
||||
/// </summary>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using NS = Newtonsoft.Json;
|
||||
@ -192,3 +193,33 @@ public class PermissiveStringEnumerableConverter : JsonConverter<IEnumerable<str
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
|
@ -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.Auth.Repositories;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Infrastructure.Dapper.AdminConsole.Repositories;
|
||||
using Bit.Infrastructure.Dapper.Auth.Repositories;
|
||||
using Bit.Infrastructure.Dapper.Billing.Repositories;
|
||||
using Bit.Infrastructure.Dapper.Repositories;
|
||||
using Bit.Infrastructure.Dapper.SecretsManager.Repositories;
|
||||
using Bit.Infrastructure.Dapper.Tools.Repositories;
|
||||
@ -48,6 +50,7 @@ public static class DapperServiceCollectionExtensions
|
||||
services.AddSingleton<IUserRepository, UserRepository>();
|
||||
services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>();
|
||||
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
|
||||
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
|
||||
|
||||
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)
|
||||
{
|
||||
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.Auth.Repositories;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
@ -7,6 +8,7 @@ using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Auth.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Billing.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Tools.Repositories;
|
||||
@ -85,6 +87,7 @@ public static class EntityFrameworkServiceCollectionExtensions
|
||||
services.AddSingleton<IUserRepository, UserRepository>();
|
||||
services.AddSingleton<IOrganizationDomainRepository, OrganizationDomainRepository>();
|
||||
services.AddSingleton<IWebAuthnCredentialRepository, WebAuthnCredentialRepository>();
|
||||
services.AddSingleton<IProviderPlanRepository, ProviderPlanRepository>();
|
||||
|
||||
if (selfHosted)
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
using AutoMapper;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||
|
||||
namespace Bit.Infrastructure.EntityFramework.Models;
|
||||
|
||||
@ -7,6 +8,7 @@ public class Transaction : Core.Entities.Transaction
|
||||
{
|
||||
public virtual Organization Organization { get; set; }
|
||||
public virtual User User { get; set; }
|
||||
public virtual Provider Provider { get; set; }
|
||||
}
|
||||
|
||||
public class TransactionMapperProfile : Profile
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider;
|
||||
using Bit.Infrastructure.EntityFramework.Auth.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Billing.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Converters;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.Infrastructure.EntityFramework.SecretsManager.Models;
|
||||
@ -65,6 +66,7 @@ public class DatabaseContext : DbContext
|
||||
public DbSet<AuthRequest> AuthRequests { get; set; }
|
||||
public DbSet<OrganizationDomain> OrganizationDomains { get; set; }
|
||||
public DbSet<WebAuthnCredential> WebAuthnCredentials { get; set; }
|
||||
public DbSet<ProviderPlan> ProviderPlans { get; set; }
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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-SelfHost' " />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="8.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="8.0.3" />
|
||||
</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