mirror of
https://github.com/bitwarden/server.git
synced 2025-02-21 02:41:21 +01:00
Merge remote-tracking branch 'origin/main' into web-push-poc
This commit is contained in:
commit
773dd5590d
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -71,6 +71,7 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
|
||||
.github/workflows/repository-management.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/test-database.yml @bitwarden/team-platform-dev
|
||||
.github/workflows/test.yml @bitwarden/team-platform-dev
|
||||
**/*Platform* @bitwarden/team-platform-dev
|
||||
|
||||
# Multiple owners - DO NOT REMOVE (BRE)
|
||||
**/packages.lock.json
|
||||
|
11
.github/renovate.json
vendored
11
.github/renovate.json
vendored
@ -12,20 +12,20 @@
|
||||
{
|
||||
"groupName": "dockerfile minor",
|
||||
"matchManagers": ["dockerfile"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
"matchUpdateTypes": ["minor"]
|
||||
},
|
||||
{
|
||||
"groupName": "docker-compose minor",
|
||||
"matchManagers": ["docker-compose"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
"matchUpdateTypes": ["minor"]
|
||||
},
|
||||
{
|
||||
"groupName": "gh minor",
|
||||
"groupName": "github-action minor",
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
"matchUpdateTypes": ["minor"]
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions", "dockerfile", "docker-compose"],
|
||||
"matchManagers": ["dockerfile", "docker-compose"],
|
||||
"commitMessagePrefix": "[deps] BRE:"
|
||||
},
|
||||
{
|
||||
@ -64,7 +64,6 @@
|
||||
"Braintree",
|
||||
"coverlet.collector",
|
||||
"CsvHelper",
|
||||
"FluentAssertions",
|
||||
"Kralizek.AutoFixture.Extensions.MockHttp",
|
||||
"Microsoft.AspNetCore.Mvc.Testing",
|
||||
"Microsoft.Extensions.Logging",
|
||||
|
32
.github/workflows/build.yml
vendored
32
.github/workflows/build.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Verify format
|
||||
run: dotnet format --verify-no-changes
|
||||
@ -81,7 +81,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
@ -120,7 +120,7 @@ jobs:
|
||||
ls -atlh ../../../
|
||||
|
||||
- name: Upload project artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: ${{ matrix.project_name }}.zip
|
||||
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
|
||||
@ -278,7 +278,7 @@ jobs:
|
||||
|
||||
- name: Build Docker image
|
||||
id: build-docker
|
||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
||||
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
|
||||
with:
|
||||
context: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
|
||||
@ -314,7 +314,7 @@ jobs:
|
||||
output-format: sarif
|
||||
|
||||
- name: Upload Grype results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||
with:
|
||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||
|
||||
@ -329,7 +329,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Log in to Azure - production subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@ -393,7 +393,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: docker-stub-US.zip
|
||||
path: docker-stub-US.zip
|
||||
@ -403,7 +403,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: docker-stub-EU.zip
|
||||
path: docker-stub-EU.zip
|
||||
@ -413,7 +413,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: docker-stub-US-sha256.txt
|
||||
path: docker-stub-US-sha256.txt
|
||||
@ -423,7 +423,7 @@ jobs:
|
||||
if: |
|
||||
github.event_name != 'pull_request_target'
|
||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: docker-stub-EU-sha256.txt
|
||||
path: docker-stub-EU-sha256.txt
|
||||
@ -447,7 +447,7 @@ jobs:
|
||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
||||
|
||||
- name: Upload Public API Swagger artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: swagger.json
|
||||
path: swagger.json
|
||||
@ -481,14 +481,14 @@ jobs:
|
||||
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
|
||||
|
||||
- name: Upload Internal API Swagger artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: internal.json
|
||||
path: internal.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Identity Swagger artifact
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: identity.json
|
||||
path: identity.json
|
||||
@ -517,7 +517,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@ -533,7 +533,7 @@ jobs:
|
||||
|
||||
- name: Upload project artifact for Windows
|
||||
if: ${{ contains(matrix.target, 'win') == true }}
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: MsSqlMigratorUtility-${{ matrix.target }}
|
||||
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe
|
||||
@ -541,7 +541,7 @@ jobs:
|
||||
|
||||
- name: Upload project artifact
|
||||
if: ${{ contains(matrix.target, 'win') == false }}
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: MsSqlMigratorUtility-${{ matrix.target }}
|
||||
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility
|
||||
|
2
.github/workflows/code-references.yml
vendored
2
.github/workflows/code-references.yml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
- name: Collect
|
||||
id: collect
|
||||
uses: launchdarkly/find-code-references-in-pull-request@d008aa4f321d8cd35314d9cb095388dcfde84439 # v2.0.0
|
||||
uses: launchdarkly/find-code-references-in-pull-request@30f4c4ab2949bbf258b797ced2fbf6dea34df9ce # v2.1.0
|
||||
with:
|
||||
project-key: default
|
||||
environment-key: dev
|
||||
|
@ -1,33 +1,14 @@
|
||||
name: Ephemeral environment cleanup
|
||||
name: Ephemeral Environment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [unlabeled]
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
validate-pr:
|
||||
name: Validate PR
|
||||
trigger-ee-updates:
|
||||
name: Trigger Ephemeral Environment updates
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
config-exists: ${{ steps.validate-config.outputs.config-exists }}
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate config exists in path
|
||||
id: validate-config
|
||||
run: |
|
||||
if [[ -f "ephemeral-environments/$GITHUB_HEAD_REF.yaml" ]]; then
|
||||
echo "Ephemeral environment config found in path, continuing."
|
||||
echo "config-exists=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
|
||||
cleanup-config:
|
||||
name: Cleanup ephemeral environment
|
||||
runs-on: ubuntu-24.04
|
||||
needs: validate-pr
|
||||
if: ${{ needs.validate-pr.outputs.config-exists }}
|
||||
if: github.event.label.name == 'ephemeral-environment'
|
||||
steps:
|
||||
- name: Log in to Azure - CI subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@ -41,7 +22,7 @@ jobs:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Trigger Ephemeral Environment cleanup
|
||||
- name: Trigger Ephemeral Environment update
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
@ -49,11 +30,9 @@ jobs:
|
||||
await github.rest.actions.createWorkflowDispatch({
|
||||
owner: 'bitwarden',
|
||||
repo: 'devops',
|
||||
workflow_id: '_ephemeral_environment_pr_manager.yml',
|
||||
workflow_id: '_update_ephemeral_tags.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
ephemeral_env_branch: process.env.GITHUB_HEAD_REF,
|
||||
cleanup_config: true,
|
||||
project: 'server'
|
||||
ephemeral_env_branch: process.env.GITHUB_HEAD_REF
|
||||
}
|
||||
})
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -85,7 +85,7 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
if: ${{ inputs.release_type != 'Dry Run' }}
|
||||
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0
|
||||
uses: ncipollo/release-action@cdcc88a9acf3ca41c16c37bb7d21b9ad48560d87 # v1.15.0
|
||||
with:
|
||||
artifacts: "docker-stub-US.zip,
|
||||
docker-stub-US-sha256.txt,
|
||||
|
7
.github/workflows/repository-management.yml
vendored
7
.github/workflows/repository-management.yml
vendored
@ -52,7 +52,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
@ -98,7 +98,7 @@ jobs:
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
@ -197,7 +197,7 @@ jobs:
|
||||
- setup
|
||||
steps:
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
|
||||
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||
@ -206,6 +206,7 @@ jobs:
|
||||
- name: Check out main branch
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: main
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
|
11
.github/workflows/scan.yml
vendored
11
.github/workflows/scan.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Scan with Checkmarx
|
||||
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
|
||||
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
|
||||
env:
|
||||
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
|
||||
with:
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
uses: github/codeql-action/upload-sarif@dd746615b3b9d728a6a37ca2045b68ca76d4841a # v3.28.8
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@ -60,7 +60,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: "zulu"
|
||||
@ -72,7 +72,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Install SonarCloud scanner
|
||||
run: dotnet tool install dotnet-sonarscanner -g
|
||||
@ -80,12 +80,11 @@ jobs:
|
||||
- name: Scan with SonarCloud
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
dotnet-sonarscanner begin /k:"${{ github.repository_owner }}_${{ github.event.repository.name }}" \
|
||||
/d:sonar.test.inclusions=test/,bitwarden_license/test/ \
|
||||
/d:sonar.exclusions=test/,bitwarden_license/test/ \
|
||||
/o:"${{ github.repository_owner }}" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \
|
||||
/d:sonar.host.url="https://sonarcloud.io"
|
||||
/d:sonar.host.url="https://sonarcloud.io" ${{ contains(github.event_name, 'pull_request') && format('/d:sonar.pullrequest.key={0}', github.event.pull_request.number) || '' }}
|
||||
dotnet build
|
||||
dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
|
2
.github/workflows/stale-bot.yml
vendored
2
.github/workflows/stale-bot.yml
vendored
@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check
|
||||
uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
|
||||
uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
with:
|
||||
stale-issue-label: "needs-reply"
|
||||
stale-pr-label: "needs-changes"
|
||||
|
12
.github/workflows/test-database.yml
vendored
12
.github/workflows/test-database.yml
vendored
@ -57,7 +57,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Restore tools
|
||||
run: dotnet tool restore
|
||||
@ -107,7 +107,7 @@ jobs:
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
|
||||
env:
|
||||
CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true"
|
||||
|
||||
|
||||
- name: Migrate MariaDB
|
||||
working-directory: "util/MySqlMigrations"
|
||||
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
|
||||
@ -186,7 +186,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@ -200,7 +200,7 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload DACPAC
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: sql.dacpac
|
||||
path: Sql.dacpac
|
||||
@ -226,7 +226,7 @@ jobs:
|
||||
shell: pwsh
|
||||
|
||||
- name: Report validation results
|
||||
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
|
||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||
with:
|
||||
name: report.xml
|
||||
path: |
|
||||
@ -237,7 +237,7 @@ jobs:
|
||||
run: |
|
||||
if grep -q "<Operations>" "report.xml"; then
|
||||
echo
|
||||
echo "Migrations are out of sync with sqlproj!"
|
||||
echo "Migration files are not in sync with the files in the Sql project. Review to make sure that any stored procedures / other db changes match with the stored procedures in the Sql project."
|
||||
exit 1
|
||||
else
|
||||
echo "Report looks good"
|
||||
|
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
@ -78,6 +78,3 @@ jobs:
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
|
||||
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
18
.vscode/extensions.json
vendored
Normal file
18
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"nick-rudenko.back-n-forth",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"MS-vsliveshare.vsliveshare",
|
||||
|
||||
"mhutchie.git-graph",
|
||||
"donjayamanne.githistory",
|
||||
"eamodio.gitlens",
|
||||
|
||||
"jakebathman.mysql-syntax",
|
||||
"ckolkman.vscode-postgres",
|
||||
|
||||
"ms-dotnettools.csharp",
|
||||
"formulahendry.dotnet-test-explorer",
|
||||
"adrianwilczynski.user-secrets"
|
||||
]
|
||||
}
|
@ -3,11 +3,17 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.1.0</Version>
|
||||
<Version>2025.2.0</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<!-- Treat it as a test project if the project hasn't set their own value and it follows our test project conventions -->
|
||||
<IsTestProject Condition="'$(IsTestProject)' == '' and ($(MSBuildProjectName.EndsWith('.Test')) or $(MSBuildProjectName.EndsWith('.IntegrationTest')))">true</IsTestProject>
|
||||
<Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' == 'true'">annotations</Nullable>
|
||||
<!-- Uncomment the below line when we are ready to enable nullable repo wide -->
|
||||
<!-- <Nullable Condition="'$(Nullable)' == '' and '$(IsTestProject)' != 'true'">enable</Nullable> -->
|
||||
<TreatWarningsAsErrors Condition="'$(TreatWarningsAsErrors)' == ''">true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
@ -64,4 +70,4 @@
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
</Project>
|
@ -125,6 +125,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Notifications.Test", "test\
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure.Dapper.Test", "test\Infrastructure.Dapper.Test\Infrastructure.Dapper.Test.csproj", "{4A725DB3-BE4F-4C23-9087-82D0610D67AF}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "test\Events.IntegrationTest\Events.IntegrationTest.csproj", "{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -313,6 +315,10 @@ Global
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@ -363,6 +369,7 @@ Global
|
||||
{81673EFB-7134-4B4B-A32F-1EA05F0EF3CE} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{90D85D8F-5577-4570-A96E-5A2E185F0F6F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
{4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
|
||||
|
@ -1,12 +1,15 @@
|
||||
using System.Globalization;
|
||||
using Bit.Commercial.Core.Billing.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
@ -24,6 +27,7 @@ using Stripe;
|
||||
namespace Bit.Commercial.Core.Billing;
|
||||
|
||||
public class ProviderBillingService(
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<ProviderBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -31,10 +35,93 @@ public class ProviderBillingService(
|
||||
IProviderInvoiceItemRepository providerInvoiceItemRepository,
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService,
|
||||
ITaxService taxService) : IProviderBillingService
|
||||
{
|
||||
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
|
||||
public async Task AddExistingOrganization(
|
||||
Provider provider,
|
||||
Organization organization,
|
||||
string key)
|
||||
{
|
||||
await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions
|
||||
{
|
||||
CancelAtPeriodEnd = false
|
||||
});
|
||||
|
||||
var subscription =
|
||||
await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId,
|
||||
new SubscriptionCancelOptions
|
||||
{
|
||||
CancellationDetails = new SubscriptionCancellationDetailsOptions
|
||||
{
|
||||
Comment = $"Organization was added to Provider with ID {provider.Id}"
|
||||
},
|
||||
InvoiceNow = true,
|
||||
Prorate = true,
|
||||
Expand = ["latest_invoice", "test_clock"]
|
||||
});
|
||||
|
||||
var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow;
|
||||
|
||||
var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now;
|
||||
|
||||
if (!wasTrialing && subscription.LatestInvoice.Status == StripeConstants.InvoiceStatus.Draft)
|
||||
{
|
||||
await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId,
|
||||
new InvoiceFinalizeOptions { AutoAdvance = true });
|
||||
}
|
||||
|
||||
var managedPlanType = await GetManagedPlanTypeAsync(provider, organization);
|
||||
|
||||
// TODO: Replace with PricingClient
|
||||
var plan = StaticStore.GetPlan(managedPlanType);
|
||||
organization.Plan = plan.Name;
|
||||
organization.PlanType = plan.Type;
|
||||
organization.MaxCollections = plan.PasswordManager.MaxCollections;
|
||||
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
|
||||
organization.UsePolicies = plan.HasPolicies;
|
||||
organization.UseSso = plan.HasSso;
|
||||
organization.UseGroups = plan.HasGroups;
|
||||
organization.UseEvents = plan.HasEvents;
|
||||
organization.UseDirectory = plan.HasDirectory;
|
||||
organization.UseTotp = plan.HasTotp;
|
||||
organization.Use2fa = plan.Has2fa;
|
||||
organization.UseApi = plan.HasApi;
|
||||
organization.UseResetPassword = plan.HasResetPassword;
|
||||
organization.SelfHost = plan.HasSelfHost;
|
||||
organization.UsersGetPremium = plan.UsersGetPremium;
|
||||
organization.UseCustomPermissions = plan.HasCustomPermissions;
|
||||
organization.UseScim = plan.HasScim;
|
||||
organization.UseKeyConnector = plan.HasKeyConnector;
|
||||
organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb;
|
||||
organization.BillingEmail = provider.BillingEmail!;
|
||||
organization.GatewaySubscriptionId = null;
|
||||
organization.ExpirationDate = null;
|
||||
organization.MaxAutoscaleSeats = null;
|
||||
organization.Status = OrganizationStatusType.Managed;
|
||||
|
||||
var providerOrganization = new ProviderOrganization
|
||||
{
|
||||
ProviderId = provider.Id,
|
||||
OrganizationId = organization.Id,
|
||||
Key = key
|
||||
};
|
||||
|
||||
await Task.WhenAll(
|
||||
organizationRepository.ReplaceAsync(organization),
|
||||
providerOrganizationRepository.CreateAsync(providerOrganization),
|
||||
ScaleSeats(provider, organization.PlanType, organization.Seats!.Value)
|
||||
);
|
||||
|
||||
await eventService.LogProviderOrganizationEventAsync(
|
||||
providerOrganization,
|
||||
EventType.ProviderOrganization_Added);
|
||||
}
|
||||
|
||||
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||
{
|
||||
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
||||
@ -206,6 +293,81 @@ public class ProviderBillingService(
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
|
||||
public async Task<IEnumerable<AddableOrganization>> GetAddableOrganizations(
|
||||
Provider provider,
|
||||
Guid userId)
|
||||
{
|
||||
var providerUser = await providerUserRepository.GetByProviderUserAsync(provider.Id, userId);
|
||||
|
||||
if (providerUser is not { Status: ProviderUserStatusType.Confirmed })
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var candidates = await organizationRepository.GetAddableToProviderByUserIdAsync(userId, provider.Type);
|
||||
|
||||
var active = (await Task.WhenAll(candidates.Select(async organization =>
|
||||
{
|
||||
var subscription = await subscriberService.GetSubscription(organization);
|
||||
return (organization, subscription);
|
||||
})))
|
||||
.Where(pair => pair.subscription is
|
||||
{
|
||||
Status:
|
||||
StripeConstants.SubscriptionStatus.Active or
|
||||
StripeConstants.SubscriptionStatus.Trialing or
|
||||
StripeConstants.SubscriptionStatus.PastDue
|
||||
}).ToList();
|
||||
|
||||
if (active.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return await Task.WhenAll(active.Select(async pair =>
|
||||
{
|
||||
var (organization, _) = pair;
|
||||
|
||||
var planName = DerivePlanName(provider, organization);
|
||||
|
||||
var addable = new AddableOrganization(
|
||||
organization.Id,
|
||||
organization.Name,
|
||||
planName,
|
||||
organization.Seats!.Value);
|
||||
|
||||
if (providerUser.Type != ProviderUserType.ServiceUser)
|
||||
{
|
||||
return addable;
|
||||
}
|
||||
|
||||
var applicablePlanType = await GetManagedPlanTypeAsync(provider, organization);
|
||||
|
||||
var requiresPurchase =
|
||||
await SeatAdjustmentResultsInPurchase(provider, applicablePlanType, organization.Seats!.Value);
|
||||
|
||||
return addable with { Disabled = requiresPurchase };
|
||||
}));
|
||||
|
||||
string DerivePlanName(Provider localProvider, Organization localOrganization)
|
||||
{
|
||||
if (localProvider.Type == ProviderType.Msp)
|
||||
{
|
||||
return localOrganization.PlanType switch
|
||||
{
|
||||
var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => "Enterprise",
|
||||
var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => "Teams",
|
||||
_ => throw new BillingException()
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Replace with PricingClient
|
||||
var plan = StaticStore.GetPlan(localOrganization.PlanType);
|
||||
return plan.Name;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ScaleSeats(
|
||||
Provider provider,
|
||||
PlanType planType,
|
||||
@ -352,12 +514,10 @@ public class ProviderBillingService(
|
||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||
}
|
||||
|
||||
customerCreateOptions.TaxIdData = taxInfo.HasTaxId
|
||||
?
|
||||
[
|
||||
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
|
||||
]
|
||||
: null;
|
||||
customerCreateOptions.TaxIdData =
|
||||
[
|
||||
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
|
||||
];
|
||||
}
|
||||
|
||||
try
|
||||
@ -584,4 +744,21 @@ public class ProviderBillingService(
|
||||
|
||||
return providerPlan;
|
||||
}
|
||||
|
||||
private async Task<PlanType> GetManagedPlanTypeAsync(
|
||||
Provider provider,
|
||||
Organization organization)
|
||||
{
|
||||
if (provider.Type == ProviderType.MultiOrganizationEnterprise)
|
||||
{
|
||||
return (await providerPlanRepository.GetByProviderId(provider.Id)).First().PlanType;
|
||||
}
|
||||
|
||||
return organization.PlanType switch
|
||||
{
|
||||
var planType when PlanConstants.TeamsPlanTypes.Contains(planType) => PlanType.TeamsMonthly,
|
||||
var planType when PlanConstants.EnterprisePlanTypes.Contains(planType) => PlanType.EnterpriseMonthly,
|
||||
_ => throw new BillingException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CsvHelper" Version="32.0.3" />
|
||||
<PackageReference Include="CsvHelper" Version="33.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
120
bitwarden_license/src/Sso/package-lock.json
generated
120
bitwarden_license/src/Sso/package-lock.json
generated
@ -779,9 +779,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
|
||||
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
|
||||
"version": "4.24.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
|
||||
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -799,9 +799,9 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001669",
|
||||
"electron-to-chromium": "^1.5.41",
|
||||
"node-releases": "^2.0.18",
|
||||
"caniuse-lite": "^1.0.30001688",
|
||||
"electron-to-chromium": "^1.5.73",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
@ -819,9 +819,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001688",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz",
|
||||
"integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==",
|
||||
"version": "1.0.30001690",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
|
||||
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -840,9 +840,9 @@
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
|
||||
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -972,16 +972,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.73",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz",
|
||||
"integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==",
|
||||
"version": "1.5.75",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
|
||||
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
|
||||
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
|
||||
"version": "5.18.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
|
||||
"integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1271,9 +1271,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
|
||||
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1792,19 +1792,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.13.0",
|
||||
"is-core-module": "^2.16.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@ -2082,17 +2085,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.10",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
|
||||
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
|
||||
"version": "5.3.11",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
|
||||
"integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.20",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^3.1.1",
|
||||
"serialize-javascript": "^6.0.1",
|
||||
"terser": "^5.26.0"
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
@ -2116,59 +2119,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ajv": "^6.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
@ -248,7 +248,7 @@ public class GroupsControllerTests : IClassFixture<ScimApplicationFactory>, IAsy
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task Post_InvalidDisplayName_BadRequest(string displayName)
|
||||
public async Task Post_InvalidDisplayName_BadRequest(string? displayName)
|
||||
{
|
||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
||||
var model = new ScimGroupRequestModel
|
||||
|
@ -324,7 +324,7 @@ public class UsersControllerTests : IClassFixture<ScimApplicationFactory>, IAsyn
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public async Task Post_InvalidEmail_BadRequest(string email)
|
||||
public async Task Post_InvalidEmail_BadRequest(string? email)
|
||||
{
|
||||
var displayName = "Test User 5";
|
||||
var externalId = "UE";
|
||||
|
@ -20,4 +20,8 @@ IDP_SP_ACS_URL=http://localhost:51822/saml2/yourOrgIdHere/Acs
|
||||
# Optional reverse proxy configuration
|
||||
# Should match server listen ports in reverse-proxy.conf
|
||||
API_PROXY_PORT=4100
|
||||
IDENTITY_PROXY_PORT=33756
|
||||
IDENTITY_PROXY_PORT=33756
|
||||
|
||||
# Optional RabbitMQ configuration
|
||||
RABBITMQ_DEFAULT_USER=bitwarden
|
||||
RABBITMQ_DEFAULT_PASS=SET_A_PASSWORD_HERE_123
|
||||
|
@ -84,6 +84,20 @@ services:
|
||||
profiles:
|
||||
- idp
|
||||
|
||||
rabbitmq:
|
||||
image: rabbitmq:management
|
||||
container_name: rabbitmq
|
||||
ports:
|
||||
- "5672:5672"
|
||||
- "15672:15672"
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq_data
|
||||
profiles:
|
||||
- rabbitmq
|
||||
|
||||
reverse-proxy:
|
||||
image: nginx:alpine
|
||||
container_name: reverse-proxy
|
||||
@ -95,7 +109,23 @@ services:
|
||||
profiles:
|
||||
- proxy
|
||||
|
||||
service-bus:
|
||||
container_name: service-bus
|
||||
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
|
||||
pull_policy: always
|
||||
volumes:
|
||||
- "./servicebusemulator_config.json:/ServiceBus_Emulator/ConfigFiles/Config.json"
|
||||
ports:
|
||||
- "5672:5672"
|
||||
environment:
|
||||
SQL_SERVER: mssql
|
||||
MSSQL_SA_PASSWORD: "${MSSQL_PASSWORD}"
|
||||
ACCEPT_EULA: "Y"
|
||||
profiles:
|
||||
- servicebus
|
||||
|
||||
volumes:
|
||||
mssql_dev_data:
|
||||
postgres_dev_data:
|
||||
mysql_dev_data:
|
||||
rabbitmq_data:
|
||||
|
@ -7,11 +7,13 @@ param(
|
||||
[switch]$mysql,
|
||||
[switch]$mssql,
|
||||
[switch]$sqlite,
|
||||
[switch]$selfhost
|
||||
[switch]$selfhost,
|
||||
[switch]$test
|
||||
)
|
||||
|
||||
# Abort on any error
|
||||
$ErrorActionPreference = "Stop"
|
||||
$currentDir = Get-Location
|
||||
|
||||
if (!$all -and !$postgres -and !$mysql -and !$sqlite) {
|
||||
$mssql = $true;
|
||||
@ -25,36 +27,62 @@ if ($all -or $postgres -or $mysql -or $sqlite) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($all -or $mssql) {
|
||||
function Get-UserSecrets {
|
||||
# The dotnet cli command sometimes adds //BEGIN and //END comments to the output, Where-Object removes comments
|
||||
# to ensure a valid json
|
||||
return dotnet user-secrets list --json --project ../src/Api | Where-Object { $_ -notmatch "^//" } | ConvertFrom-Json
|
||||
}
|
||||
|
||||
if ($selfhost) {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
|
||||
$envName = "self-host"
|
||||
} else {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
|
||||
$envName = "cloud"
|
||||
}
|
||||
|
||||
Write-Host "Starting Microsoft SQL Server Migrations for $envName"
|
||||
|
||||
dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString"
|
||||
function Get-UserSecrets {
|
||||
# The dotnet cli command sometimes adds //BEGIN and //END comments to the output, Where-Object removes comments
|
||||
# to ensure a valid json
|
||||
return dotnet user-secrets list --json --project "$currentDir/../src/Api" | Where-Object { $_ -notmatch "^//" } | ConvertFrom-Json
|
||||
}
|
||||
|
||||
$currentDir = Get-Location
|
||||
if ($all -or $mssql) {
|
||||
if ($all -or !$test) {
|
||||
if ($selfhost) {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'dev:selfHostOverride:globalSettings:sqlServer:connectionString'
|
||||
$envName = "self-host"
|
||||
} else {
|
||||
$msSqlConnectionString = $(Get-UserSecrets).'globalSettings:sqlServer:connectionString'
|
||||
$envName = "cloud"
|
||||
}
|
||||
|
||||
Foreach ($item in @(@($mysql, "MySQL", "MySqlMigrations"), @($postgres, "PostgreSQL", "PostgresMigrations"), @($sqlite, "SQLite", "SqliteMigrations"))) {
|
||||
Write-Host "Starting Microsoft SQL Server Migrations for $envName"
|
||||
dotnet run --project ../util/MsSqlMigratorUtility/ "$msSqlConnectionString"
|
||||
}
|
||||
|
||||
if ($all -or $test) {
|
||||
$testMsSqlConnectionString = $(Get-UserSecrets).'databases:3:connectionString'
|
||||
if ($testMsSqlConnectionString) {
|
||||
$testEnvName = "test databases"
|
||||
Write-Host "Starting Microsoft SQL Server Migrations for $testEnvName"
|
||||
dotnet run --project ../util/MsSqlMigratorUtility/ "$testMsSqlConnectionString"
|
||||
} else {
|
||||
Write-Host "Connection string for a test MSSQL database not found in secrets.json!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Foreach ($item in @(
|
||||
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
|
||||
@($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0),
|
||||
@($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1)
|
||||
)) {
|
||||
if (!$item[0] -and !$all) {
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Starting $($item[1]) Migrations"
|
||||
Set-Location "$currentDir/../util/$($item[2])/"
|
||||
dotnet ef database update
|
||||
if(!$test -or $all) {
|
||||
Write-Host "Starting $($item[1]) Migrations"
|
||||
$connectionString = $(Get-UserSecrets)."globalSettings:$($item[3]):connectionString"
|
||||
dotnet ef database update --connection "$connectionString"
|
||||
}
|
||||
if ($test -or $all) {
|
||||
$testConnectionString = $(Get-UserSecrets)."databases:$($item[4]):connectionString"
|
||||
if ($testConnectionString) {
|
||||
Write-Host "Starting $($item[1]) Migrations for test databases"
|
||||
dotnet ef database update --connection "$testConnectionString"
|
||||
} else {
|
||||
Write-Host "Connection string for a test $($item[1]) database not found in secrets.json!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set-Location "$currentDir"
|
||||
|
@ -21,7 +21,7 @@
|
||||
"connectionString": "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev"
|
||||
},
|
||||
"sqlite": {
|
||||
"connectionString": "Data Source=/path/to/bitwardenServer/repository/server/dev/db/bitwarden.sqlite"
|
||||
"connectionString": "Data Source=/path/to/bitwardenServer/repository/server/dev/db/bitwarden.db"
|
||||
},
|
||||
"identityServer": {
|
||||
"certificateThumbprint": "<your Identity certificate thumbprint with no spaces>"
|
||||
|
38
dev/servicebusemulator_config.json
Normal file
38
dev/servicebusemulator_config.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"UserConfig": {
|
||||
"Namespaces": [
|
||||
{
|
||||
"Name": "sbemulatorns",
|
||||
"Queues": [
|
||||
{
|
||||
"Name": "queue.1",
|
||||
"Properties": {
|
||||
"DeadLetteringOnMessageExpiration": false,
|
||||
"DefaultMessageTimeToLive": "PT1H",
|
||||
"DuplicateDetectionHistoryTimeWindow": "PT20S",
|
||||
"ForwardDeadLetteredMessagesTo": "",
|
||||
"ForwardTo": "",
|
||||
"LockDuration": "PT1M",
|
||||
"MaxDeliveryCount": 3,
|
||||
"RequiresDuplicateDetection": false,
|
||||
"RequiresSession": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"Topics": [
|
||||
{
|
||||
"Name": "event-logging",
|
||||
"Subscriptions": [
|
||||
{
|
||||
"Name": "events-write-subscription"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Logging": {
|
||||
"Type": "File"
|
||||
}
|
||||
}
|
||||
}
|
@ -3,9 +3,9 @@ using Bit.Admin.AdminConsole.Models;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.Providers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
@ -57,6 +57,7 @@ public class OrganizationsController : Controller
|
||||
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IOrganizationInitiateDeleteCommand _organizationInitiateDeleteCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationService organizationService,
|
||||
@ -83,7 +84,8 @@ public class OrganizationsController : Controller
|
||||
IProviderOrganizationRepository providerOrganizationRepository,
|
||||
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
|
||||
IProviderBillingService providerBillingService,
|
||||
IFeatureService featureService)
|
||||
IFeatureService featureService,
|
||||
IOrganizationInitiateDeleteCommand organizationInitiateDeleteCommand)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -110,6 +112,7 @@ public class OrganizationsController : Controller
|
||||
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
|
||||
_providerBillingService = providerBillingService;
|
||||
_featureService = featureService;
|
||||
_organizationInitiateDeleteCommand = organizationInitiateDeleteCommand;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Org_List_View)]
|
||||
@ -306,7 +309,7 @@ public class OrganizationsController : Controller
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Org_Delete)]
|
||||
[RequirePermission(Permission.Org_RequestDelete)]
|
||||
public async Task<IActionResult> DeleteInitiation(Guid id, OrganizationInitiateDeleteModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
@ -320,7 +323,7 @@ public class OrganizationsController : Controller
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization != null)
|
||||
{
|
||||
await _organizationService.InitiateDeleteAsync(organization, model.AdminEmail);
|
||||
await _organizationInitiateDeleteCommand.InitiateDeleteAsync(organization, model.AdminEmail);
|
||||
TempData["Success"] = "The request to initiate deletion of the organization has been sent.";
|
||||
}
|
||||
}
|
||||
@ -418,6 +421,11 @@ public class OrganizationsController : Controller
|
||||
|
||||
private void UpdateOrganization(Organization organization, OrganizationEditModel model)
|
||||
{
|
||||
if (_accessControlService.UserHasPermission(Permission.Org_Name_Edit))
|
||||
{
|
||||
organization.Name = WebUtility.HtmlEncode(model.Name);
|
||||
}
|
||||
|
||||
if (_accessControlService.UserHasPermission(Permission.Org_CheckEnabledBox))
|
||||
{
|
||||
organization.Enabled = model.Enabled;
|
||||
@ -476,14 +484,6 @@ public class OrganizationsController : Controller
|
||||
Organization organization,
|
||||
OrganizationEditModel update)
|
||||
{
|
||||
var scaleMSPOnClientOrganizationUpdate =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate);
|
||||
|
||||
if (!scaleMSPOnClientOrganizationUpdate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
|
||||
// No scaling required
|
||||
|
@ -10,4 +10,6 @@ public class OrganizationsModel : PagedModel<Organization>
|
||||
public bool? Paid { get; set; }
|
||||
public string Action { get; set; }
|
||||
public bool SelfHosted { get; set; }
|
||||
|
||||
public double StorageGB(Organization org) => org.Storage.HasValue ? Math.Round(org.Storage.Value / 1073741824D, 2) : 0;
|
||||
}
|
||||
|
@ -10,6 +10,7 @@
|
||||
var canViewOrganizationInformation = AccessControlService.UserHasPermission(Permission.Org_OrgInformation_View);
|
||||
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.Org_BillingInformation_View);
|
||||
var canInitiateTrial = AccessControlService.UserHasPermission(Permission.Org_InitiateTrial);
|
||||
var canRequestDelete = AccessControlService.UserHasPermission(Permission.Org_RequestDelete);
|
||||
var canDelete = AccessControlService.UserHasPermission(Permission.Org_Delete);
|
||||
var canUnlinkFromProvider = AccessControlService.UserHasPermission(Permission.Provider_Edit);
|
||||
}
|
||||
@ -120,12 +121,15 @@
|
||||
Unlink provider
|
||||
</button>
|
||||
}
|
||||
@if (canDelete)
|
||||
@if (canRequestDelete)
|
||||
{
|
||||
<form asp-action="DeleteInitiation" asp-route-id="@Model.Organization.Id" id="initiate-delete-form">
|
||||
<input type="hidden" name="AdminEmail" id="AdminEmail" />
|
||||
<button class="btn btn-danger me-2" type="submit">Request Delete</button>
|
||||
</form>
|
||||
}
|
||||
@if (canDelete)
|
||||
{
|
||||
<form asp-action="Delete" asp-route-id="@Model.Organization.Id"
|
||||
onsubmit="return confirm('Are you sure you want to hard delete this organization?')">
|
||||
<button class="btn btn-outline-danger" type="submit">Delete</button>
|
||||
|
@ -81,16 +81,7 @@
|
||||
<i class="fa fa-smile-o fa-lg fa-fw text-body-secondary" title="Freeloader"></i>
|
||||
}
|
||||
}
|
||||
@if(org.MaxStorageGb.HasValue && org.MaxStorageGb > 1)
|
||||
{
|
||||
<i class="fa fa-plus-square fa-lg fa-fw"
|
||||
title="Additional Storage, @(org.MaxStorageGb - 1) GB"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="fa fa-plus-square-o fa-lg fa-fw text-body-secondary"
|
||||
title="No Additional Storage"></i>
|
||||
}
|
||||
<i class="fa fa-hdd-o fa-lg fa-fw" title="Used Storage, @Model.StorageGB(org) GB"></i>
|
||||
@if(org.Enabled)
|
||||
{
|
||||
<i class="fa fa-check-circle fa-lg fa-fw"
|
||||
|
@ -12,6 +12,7 @@
|
||||
var canViewBilling = AccessControlService.UserHasPermission(Permission.Org_Billing_View);
|
||||
var canViewPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_View);
|
||||
var canViewLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_View);
|
||||
var canEditName = AccessControlService.UserHasPermission(Permission.Org_Name_Edit);
|
||||
var canCheckEnabled = AccessControlService.UserHasPermission(Permission.Org_CheckEnabledBox);
|
||||
var canEditPlan = AccessControlService.UserHasPermission(Permission.Org_Plan_Edit);
|
||||
var canEditLicensing = AccessControlService.UserHasPermission(Permission.Org_Licensing_Edit);
|
||||
@ -28,7 +29,7 @@
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" asp-for="Name"></label>
|
||||
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required>
|
||||
<input type="text" class="form-control" asp-for="Name" value="@Model.Name" required disabled="@(canEditName ? null : "disabled")">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,19 +3,18 @@ using System.Text.Json;
|
||||
using Bit.Admin.Enums;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TaxRate = Bit.Core.Entities.TaxRate;
|
||||
|
||||
namespace Bit.Admin.Controllers;
|
||||
|
||||
@ -32,7 +31,6 @@ public class ToolsController : Controller
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IPaymentService _paymentService;
|
||||
private readonly ITaxRateRepository _taxRateRepository;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IWebHostEnvironment _environment;
|
||||
|
||||
@ -45,7 +43,6 @@ public class ToolsController : Controller
|
||||
IInstallationRepository installationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
ITaxRateRepository taxRateRepository,
|
||||
IPaymentService paymentService,
|
||||
IStripeAdapter stripeAdapter,
|
||||
IWebHostEnvironment environment)
|
||||
@ -58,7 +55,6 @@ public class ToolsController : Controller
|
||||
_installationRepository = installationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_taxRateRepository = taxRateRepository;
|
||||
_paymentService = paymentService;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_environment = environment;
|
||||
@ -225,7 +221,6 @@ public class ToolsController : Controller
|
||||
return RedirectToAction("Edit", "Organizations", new { id = model.OrganizationId.Value });
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.PromoteProviderServiceUserTool)]
|
||||
[RequirePermission(Permission.Tools_PromoteProviderServiceUser)]
|
||||
public IActionResult PromoteProviderServiceUser()
|
||||
{
|
||||
@ -234,7 +229,6 @@ public class ToolsController : Controller
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequireFeature(FeatureFlagKeys.PromoteProviderServiceUserTool)]
|
||||
[RequirePermission(Permission.Tools_PromoteProviderServiceUser)]
|
||||
public async Task<IActionResult> PromoteProviderServiceUser(PromoteProviderServiceUserModel model)
|
||||
{
|
||||
@ -345,165 +339,6 @@ public class ToolsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_ManageTaxRates)]
|
||||
public async Task<IActionResult> TaxRate(int page = 1, int count = 25)
|
||||
{
|
||||
if (page < 1)
|
||||
{
|
||||
page = 1;
|
||||
}
|
||||
|
||||
if (count < 1)
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
|
||||
var skip = (page - 1) * count;
|
||||
var rates = await _taxRateRepository.SearchAsync(skip, count);
|
||||
return View(new TaxRatesModel
|
||||
{
|
||||
Items = rates.ToList(),
|
||||
Page = page,
|
||||
Count = count
|
||||
});
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_ManageTaxRates)]
|
||||
public async Task<IActionResult> TaxRateAddEdit(string stripeTaxRateId = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(stripeTaxRateId))
|
||||
{
|
||||
return View(new TaxRateAddEditModel());
|
||||
}
|
||||
|
||||
var rate = await _taxRateRepository.GetByIdAsync(stripeTaxRateId);
|
||||
var model = new TaxRateAddEditModel()
|
||||
{
|
||||
StripeTaxRateId = stripeTaxRateId,
|
||||
Country = rate.Country,
|
||||
State = rate.State,
|
||||
PostalCode = rate.PostalCode,
|
||||
Rate = rate.Rate
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Tools_ManageTaxRates)]
|
||||
public async Task<IActionResult> TaxRateUpload(IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(file));
|
||||
}
|
||||
|
||||
// Build rates and validate them first before updating DB & Stripe
|
||||
var taxRateUpdates = new List<TaxRate>();
|
||||
var currentTaxRates = await _taxRateRepository.GetAllActiveAsync();
|
||||
using var reader = new StreamReader(file.OpenReadStream());
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = await reader.ReadLineAsync();
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var taxParts = line.Split(',');
|
||||
if (taxParts.Length < 2)
|
||||
{
|
||||
throw new Exception($"This line is not in the format of <postal code>,<rate>,<state code>,<country code>: {line}");
|
||||
}
|
||||
var postalCode = taxParts[0].Trim();
|
||||
if (string.IsNullOrWhiteSpace(postalCode))
|
||||
{
|
||||
throw new Exception($"'{line}' is not valid, the first element must contain a postal code.");
|
||||
}
|
||||
if (!decimal.TryParse(taxParts[1], out var rate) || rate <= 0M || rate > 100)
|
||||
{
|
||||
throw new Exception($"{taxParts[1]} is not a valid rate/decimal for {postalCode}");
|
||||
}
|
||||
var state = taxParts.Length > 2 ? taxParts[2] : null;
|
||||
var country = (taxParts.Length > 3 ? taxParts[3] : null);
|
||||
if (string.IsNullOrWhiteSpace(country))
|
||||
{
|
||||
country = "US";
|
||||
}
|
||||
var taxRate = currentTaxRates.FirstOrDefault(r => r.Country == country && r.PostalCode == postalCode) ??
|
||||
new TaxRate
|
||||
{
|
||||
Country = country,
|
||||
PostalCode = postalCode,
|
||||
Active = true,
|
||||
};
|
||||
taxRate.Rate = rate;
|
||||
taxRate.State = state ?? taxRate.State;
|
||||
taxRateUpdates.Add(taxRate);
|
||||
}
|
||||
|
||||
foreach (var taxRate in taxRateUpdates)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(taxRate.Id))
|
||||
{
|
||||
await _paymentService.UpdateTaxRateAsync(taxRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _paymentService.CreateTaxRateAsync(taxRate);
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction("TaxRate");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.Tools_ManageTaxRates)]
|
||||
public async Task<IActionResult> TaxRateAddEdit(TaxRateAddEditModel model)
|
||||
{
|
||||
var existingRateCheck = await _taxRateRepository.GetByLocationAsync(new TaxRate() { Country = model.Country, PostalCode = model.PostalCode });
|
||||
if (existingRateCheck.Any())
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.PostalCode), "A tax rate already exists for this Country/Postal Code combination.");
|
||||
}
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var taxRate = new TaxRate()
|
||||
{
|
||||
Id = model.StripeTaxRateId,
|
||||
Country = model.Country,
|
||||
State = model.State,
|
||||
PostalCode = model.PostalCode,
|
||||
Rate = model.Rate
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(model.StripeTaxRateId))
|
||||
{
|
||||
await _paymentService.UpdateTaxRateAsync(taxRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _paymentService.CreateTaxRateAsync(taxRate);
|
||||
}
|
||||
|
||||
return RedirectToAction("TaxRate");
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_ManageTaxRates)]
|
||||
public async Task<IActionResult> TaxRateArchive(string stripeTaxRateId)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(stripeTaxRateId))
|
||||
{
|
||||
await _paymentService.ArchiveTaxRateAsync(new TaxRate() { Id = stripeTaxRateId });
|
||||
}
|
||||
|
||||
return RedirectToAction("TaxRate");
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.Tools_ManageStripeSubscriptions)]
|
||||
public async Task<IActionResult> StripeSubscriptions(StripeSubscriptionListOptions options)
|
||||
{
|
||||
|
@ -107,7 +107,8 @@ public class UsersController : Controller
|
||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain));
|
||||
var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id);
|
||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -162,6 +163,22 @@ public class UsersController : Controller
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RequirePermission(Permission.User_NewDeviceException_Edit)]
|
||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
||||
public async Task<IActionResult> ToggleNewDeviceVerification(Guid id)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id);
|
||||
if (user == null)
|
||||
{
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
await _userService.ToggleNewDeviceVerificationException(user.Id);
|
||||
return RedirectToAction("Edit", new { id });
|
||||
}
|
||||
|
||||
// TODO: Feature flag to be removed in PM-14207
|
||||
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
|
||||
{
|
||||
|
@ -17,13 +17,16 @@ public enum Permission
|
||||
User_Billing_View,
|
||||
User_Billing_Edit,
|
||||
User_Billing_LaunchGateway,
|
||||
User_NewDeviceException_Edit,
|
||||
|
||||
Org_List_View,
|
||||
Org_OrgInformation_View,
|
||||
Org_GeneralDetails_View,
|
||||
Org_Name_Edit,
|
||||
Org_CheckEnabledBox,
|
||||
Org_BusinessInformation_View,
|
||||
Org_InitiateTrial,
|
||||
Org_RequestDelete,
|
||||
Org_Delete,
|
||||
Org_BillingInformation_View,
|
||||
Org_BillingInformation_DownloadInvoice,
|
||||
|
@ -1,10 +0,0 @@
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class TaxRateAddEditModel
|
||||
{
|
||||
public string StripeTaxRateId { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string State { get; set; }
|
||||
public string PostalCode { get; set; }
|
||||
public decimal Rate { get; set; }
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Admin.Models;
|
||||
|
||||
public class TaxRatesModel : PagedModel<TaxRate>
|
||||
{
|
||||
public string Message { get; set; }
|
||||
}
|
@ -18,10 +18,13 @@ public class UserEditModel
|
||||
BillingInfo billingInfo,
|
||||
BillingHistoryInfo billingHistoryInfo,
|
||||
GlobalSettings globalSettings,
|
||||
bool? claimedAccount)
|
||||
bool? claimedAccount,
|
||||
bool? activeNewDeviceVerificationException)
|
||||
{
|
||||
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, claimedAccount);
|
||||
|
||||
ActiveNewDeviceVerificationException = activeNewDeviceVerificationException ?? false;
|
||||
|
||||
BillingInfo = billingInfo;
|
||||
BillingHistoryInfo = billingHistoryInfo;
|
||||
BraintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
@ -44,6 +47,8 @@ public class UserEditModel
|
||||
public string RandomLicenseKey => CoreHelpers.SecureRandomString(20);
|
||||
public string OneYearExpirationDate => DateTime.Now.AddYears(1).ToString("yyyy-MM-ddTHH:mm");
|
||||
public string BraintreeMerchantId { get; init; }
|
||||
public bool ActiveNewDeviceVerificationException { get; init; }
|
||||
|
||||
|
||||
[Display(Name = "Name")]
|
||||
public string Name { get; init; }
|
||||
|
@ -12,7 +12,6 @@ public static class RolePermissionMapping
|
||||
Permission.User_List_View,
|
||||
Permission.User_UserInformation_View,
|
||||
Permission.User_GeneralDetails_View,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.User_Delete,
|
||||
Permission.User_UpgradePremium,
|
||||
Permission.User_BillingInformation_View,
|
||||
@ -24,12 +23,16 @@ public static class RolePermissionMapping
|
||||
Permission.User_Billing_View,
|
||||
Permission.User_Billing_Edit,
|
||||
Permission.User_Billing_LaunchGateway,
|
||||
Permission.User_NewDeviceException_Edit,
|
||||
Permission.Org_Name_Edit,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
Permission.Org_BusinessInformation_View,
|
||||
Permission.Org_InitiateTrial,
|
||||
Permission.Org_Delete,
|
||||
Permission.Org_RequestDelete,
|
||||
Permission.Org_BillingInformation_View,
|
||||
Permission.Org_BillingInformation_DownloadInvoice,
|
||||
Permission.Org_Plan_View,
|
||||
@ -56,7 +59,6 @@ public static class RolePermissionMapping
|
||||
Permission.User_List_View,
|
||||
Permission.User_UserInformation_View,
|
||||
Permission.User_GeneralDetails_View,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.User_Delete,
|
||||
Permission.User_UpgradePremium,
|
||||
Permission.User_BillingInformation_View,
|
||||
@ -69,11 +71,15 @@ public static class RolePermissionMapping
|
||||
Permission.User_Billing_View,
|
||||
Permission.User_Billing_Edit,
|
||||
Permission.User_Billing_LaunchGateway,
|
||||
Permission.User_NewDeviceException_Edit,
|
||||
Permission.Org_Name_Edit,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
Permission.Org_BusinessInformation_View,
|
||||
Permission.Org_Delete,
|
||||
Permission.Org_RequestDelete,
|
||||
Permission.Org_BillingInformation_View,
|
||||
Permission.Org_BillingInformation_DownloadInvoice,
|
||||
Permission.Org_BillingInformation_CreateEditTransaction,
|
||||
@ -104,7 +110,6 @@ public static class RolePermissionMapping
|
||||
Permission.User_List_View,
|
||||
Permission.User_UserInformation_View,
|
||||
Permission.User_GeneralDetails_View,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.User_UpgradePremium,
|
||||
Permission.User_BillingInformation_View,
|
||||
Permission.User_BillingInformation_DownloadInvoice,
|
||||
@ -112,9 +117,11 @@ public static class RolePermissionMapping
|
||||
Permission.User_Licensing_View,
|
||||
Permission.User_Billing_View,
|
||||
Permission.User_Billing_LaunchGateway,
|
||||
Permission.User_NewDeviceException_Edit,
|
||||
Permission.Org_Name_Edit,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_Delete,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
Permission.Org_BusinessInformation_View,
|
||||
Permission.Org_BillingInformation_View,
|
||||
@ -124,6 +131,7 @@ public static class RolePermissionMapping
|
||||
Permission.Org_Licensing_View,
|
||||
Permission.Org_Billing_View,
|
||||
Permission.Org_Billing_LaunchGateway,
|
||||
Permission.Org_RequestDelete,
|
||||
Permission.Provider_List_View,
|
||||
Permission.Provider_View
|
||||
}
|
||||
@ -133,7 +141,6 @@ public static class RolePermissionMapping
|
||||
Permission.User_List_View,
|
||||
Permission.User_UserInformation_View,
|
||||
Permission.User_GeneralDetails_View,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.User_UpgradePremium,
|
||||
Permission.User_BillingInformation_View,
|
||||
Permission.User_BillingInformation_DownloadInvoice,
|
||||
@ -144,6 +151,8 @@ public static class RolePermissionMapping
|
||||
Permission.User_Billing_View,
|
||||
Permission.User_Billing_Edit,
|
||||
Permission.User_Billing_LaunchGateway,
|
||||
Permission.Org_Name_Edit,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
@ -157,7 +166,7 @@ public static class RolePermissionMapping
|
||||
Permission.Org_Billing_View,
|
||||
Permission.Org_Billing_Edit,
|
||||
Permission.Org_Billing_LaunchGateway,
|
||||
Permission.Org_Delete,
|
||||
Permission.Org_RequestDelete,
|
||||
Permission.Provider_Edit,
|
||||
Permission.Provider_View,
|
||||
Permission.Provider_List_View,
|
||||
@ -175,12 +184,13 @@ public static class RolePermissionMapping
|
||||
Permission.User_List_View,
|
||||
Permission.User_UserInformation_View,
|
||||
Permission.User_GeneralDetails_View,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.User_BillingInformation_View,
|
||||
Permission.User_BillingInformation_DownloadInvoice,
|
||||
Permission.User_Premium_View,
|
||||
Permission.User_Licensing_View,
|
||||
Permission.User_Licensing_Edit,
|
||||
Permission.Org_Name_Edit,
|
||||
Permission.Org_CheckEnabledBox,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
|
@ -1,10 +1,8 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Core
|
||||
|
||||
@inject SignInManager<IdentityUser> SignInManager
|
||||
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
|
||||
@{
|
||||
var canViewUsers = AccessControlService.UserHasPermission(Permission.User_List_View);
|
||||
@ -13,16 +11,14 @@
|
||||
var canChargeBraintree = AccessControlService.UserHasPermission(Permission.Tools_ChargeBrainTreeCustomer);
|
||||
var canCreateTransaction = AccessControlService.UserHasPermission(Permission.Tools_CreateEditTransaction);
|
||||
var canPromoteAdmin = AccessControlService.UserHasPermission(Permission.Tools_PromoteAdmin);
|
||||
var canPromoteProviderServiceUser = FeatureService.IsEnabled(FeatureFlagKeys.PromoteProviderServiceUserTool) &&
|
||||
AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);
|
||||
var canPromoteProviderServiceUser = AccessControlService.UserHasPermission(Permission.Tools_PromoteProviderServiceUser);
|
||||
var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile);
|
||||
var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates);
|
||||
var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions);
|
||||
var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents);
|
||||
var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders);
|
||||
|
||||
var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canPromoteProviderServiceUser ||
|
||||
canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions;
|
||||
canGenerateLicense || canManageStripeSubscriptions;
|
||||
}
|
||||
|
||||
<!DOCTYPE html>
|
||||
@ -107,12 +103,6 @@
|
||||
Generate License
|
||||
</a>
|
||||
}
|
||||
@if (canManageTaxRates)
|
||||
{
|
||||
<a class="dropdown-item" asp-controller="Tools" asp-action="TaxRate">
|
||||
Manage Tax Rates
|
||||
</a>
|
||||
}
|
||||
@if (canManageStripeSubscriptions)
|
||||
{
|
||||
<a class="dropdown-item" asp-controller="Tools" asp-action="StripeSubscriptions">
|
||||
|
@ -1,127 +0,0 @@
|
||||
@model TaxRatesModel
|
||||
@{
|
||||
ViewData["Title"] = "Tax Rates";
|
||||
}
|
||||
|
||||
<h1>Manage Tax Rates</h1>
|
||||
|
||||
<h2>Bulk Upload Tax Rates</h2>
|
||||
<section>
|
||||
<p>
|
||||
Upload a CSV file containing multiple tax rates in bulk in order to update existing rates by country
|
||||
and postal code OR to create new rates where a currently active rate is not found already.
|
||||
</p>
|
||||
<p>CSV Upload Format</p>
|
||||
<ul>
|
||||
<li><b>Postal Code</b> (required) - The postal code for the tax rate.</li>
|
||||
<li><b>Rate</b> (required) - The effective tax rate for this postal code.</li>
|
||||
<li><b>State</b> (<i>optional</i>) - The ISO-2 character code for the state. Optional but recommended.</li>
|
||||
<li><b>Country</b> (<i>optional</i>) - The ISO-2 character country code, defaults to "US" if not provided.</li>
|
||||
</ul>
|
||||
<p>Example (white-space is ignored):</p>
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
<pre class="mb-0">87654,8.25,FL,US
|
||||
22334,8.5,CA
|
||||
11223,7</pre>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" enctype="multipart/form-data" asp-action="TaxRateUpload">
|
||||
<div class="mb-3">
|
||||
<input type="file" class="form-control" name="file" />
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="submit" value="Upload" class="btn btn-primary" />
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<hr class="my-4">
|
||||
<h2>View & Manage Tax Rates</h2>
|
||||
<a class="btn btn-primary mb-3" asp-controller="Tools" asp-action="TaxRateAddEdit">Add a Rate</a>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 190px;">Id</th>
|
||||
<th style="width: 80px;">Country</th>
|
||||
<th style="width: 80px;">State</th>
|
||||
<th style="width: 150px;">Postal Code</th>
|
||||
<th style="width: 160px;">Tax Rate</th>
|
||||
<th style="width: 80px;"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!Model.Items.Any())
|
||||
{
|
||||
<tr>
|
||||
<td colspan="6">No results to list.</td>
|
||||
</tr>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach(var rate in Model.Items)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
@{
|
||||
var taxRateToEdit = new Dictionary<string, string>
|
||||
{
|
||||
{ "id", rate.Id },
|
||||
{ "stripeTaxRateId", rate.Id }
|
||||
};
|
||||
}
|
||||
<a asp-controller="Tools" asp-action="TaxRateAddEdit" asp-all-route-data="taxRateToEdit">@rate.Id</a>
|
||||
</td>
|
||||
<td>
|
||||
@rate.Country
|
||||
</td>
|
||||
<td>
|
||||
@rate.State
|
||||
</td>
|
||||
<td>
|
||||
@rate.PostalCode
|
||||
</td>
|
||||
<td>
|
||||
@rate.Rate%
|
||||
</td>
|
||||
<td>
|
||||
<a class="delete-button" data-id="@rate.Id" asp-controller="Tools" asp-action="TaxRateArchive" asp-route-stripeTaxRateId="@rate.Id">
|
||||
<i class="fa fa-trash fa-lg fa-fw"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<nav aria-label="Tax rates pagination">
|
||||
<ul class="pagination">
|
||||
@if(Model.PreviousPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-controller="Tools" asp-action="TaxRate" asp-route-page="@Model.PreviousPage.Value" asp-route-count="@Model.Count">Previous</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||
</li>
|
||||
}
|
||||
@if(Model.NextPage.HasValue)
|
||||
{
|
||||
<li class="page-item">
|
||||
<a class="page-link" asp-controller="Tools" asp-action="TaxRate" asp-route-page="@Model.NextPage.Value" asp-route-count="@Model.Count">Next</a>
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
<li class="page-item disabled">
|
||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
@ -1,356 +0,0 @@
|
||||
@model TaxRateAddEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Add/Edit Tax Rate";
|
||||
}
|
||||
|
||||
|
||||
<h1>@(string.IsNullOrWhiteSpace(Model.StripeTaxRateId) ? "Create" : "Edit") Tax Rate</h1>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(Model.StripeTaxRateId))
|
||||
{
|
||||
<p>Note: Updating a Tax Rate archives the currently selected rate and creates a new rate with a new ID. The previous data still exists in a disabled state.</p>
|
||||
}
|
||||
|
||||
<form method="post">
|
||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||
<input type="hidden" asp-for="StripeTaxRateId">
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="Country"></label>
|
||||
<select asp-for="Country" class="form-control" required>
|
||||
<option value="">-- Select --</option>
|
||||
<option value="US">United States</option>
|
||||
<option value="CN">China</option>
|
||||
<option value="FR">France</option>
|
||||
<option value="DE">Germany</option>
|
||||
<option value="CA">Canada</option>
|
||||
<option value="GB">United Kingdom</option>
|
||||
<option value="AU">Australia</option>
|
||||
<option value="IN">India</option>
|
||||
<option value="-" disabled></option>
|
||||
<option value="AF">Afghanistan</option>
|
||||
<option value="AX">Åland Islands</option>
|
||||
<option value="AL">Albania</option>
|
||||
<option value="DZ">Algeria</option>
|
||||
<option value="AS">American Samoa</option>
|
||||
<option value="AD">Andorra</option>
|
||||
<option value="AO">Angola</option>
|
||||
<option value="AI">Anguilla</option>
|
||||
<option value="AQ">Antarctica</option>
|
||||
<option value="AG">Antigua and Barbuda</option>
|
||||
<option value="AR">Argentina</option>
|
||||
<option value="AM">Armenia</option>
|
||||
<option value="AW">Aruba</option>
|
||||
<option value="AT">Austria</option>
|
||||
<option value="AZ">Azerbaijan</option>
|
||||
<option value="BS">Bahamas</option>
|
||||
<option value="BH">Bahrain</option>
|
||||
<option value="BD">Bangladesh</option>
|
||||
<option value="BB">Barbados</option>
|
||||
<option value="BY">Belarus</option>
|
||||
<option value="BE">Belgium</option>
|
||||
<option value="BZ">Belize</option>
|
||||
<option value="BJ">Benin</option>
|
||||
<option value="BM">Bermuda</option>
|
||||
<option value="BT">Bhutan</option>
|
||||
<option value="BO">Bolivia, Plurinational State of</option>
|
||||
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
|
||||
<option value="BA">Bosnia and Herzegovina</option>
|
||||
<option value="BW">Botswana</option>
|
||||
<option value="BV">Bouvet Island</option>
|
||||
<option value="BR">Brazil</option>
|
||||
<option value="IO">British Indian Ocean Territory</option>
|
||||
<option value="BN">Brunei Darussalam</option>
|
||||
<option value="BG">Bulgaria</option>
|
||||
<option value="BF">Burkina Faso</option>
|
||||
<option value="BI">Burundi</option>
|
||||
<option value="KH">Cambodia</option>
|
||||
<option value="CM">Cameroon</option>
|
||||
<option value="CV">Cape Verde</option>
|
||||
<option value="KY">Cayman Islands</option>
|
||||
<option value="CF">Central African Republic</option>
|
||||
<option value="TD">Chad</option>
|
||||
<option value="CL">Chile</option>
|
||||
<option value="CX">Christmas Island</option>
|
||||
<option value="CC">Cocos (Keeling) Islands</option>
|
||||
<option value="CO">Colombia</option>
|
||||
<option value="KM">Comoros</option>
|
||||
<option value="CG">Congo</option>
|
||||
<option value="CD">Congo, the Democratic Republic of the</option>
|
||||
<option value="CK">Cook Islands</option>
|
||||
<option value="CR">Costa Rica</option>
|
||||
<option value="CI">Côte d'Ivoire</option>
|
||||
<option value="HR">Croatia</option>
|
||||
<option value="CU">Cuba</option>
|
||||
<option value="CW">Curaçao</option>
|
||||
<option value="CY">Cyprus</option>
|
||||
<option value="CZ">Czech Republic</option>
|
||||
<option value="DK">Denmark</option>
|
||||
<option value="DJ">Djibouti</option>
|
||||
<option value="DM">Dominica</option>
|
||||
<option value="DO">Dominican Republic</option>
|
||||
<option value="EC">Ecuador</option>
|
||||
<option value="EG">Egypt</option>
|
||||
<option value="SV">El Salvador</option>
|
||||
<option value="GQ">Equatorial Guinea</option>
|
||||
<option value="ER">Eritrea</option>
|
||||
<option value="EE">Estonia</option>
|
||||
<option value="ET">Ethiopia</option>
|
||||
<option value="FK">Falkland Islands (Malvinas)</option>
|
||||
<option value="FO">Faroe Islands</option>
|
||||
<option value="FJ">Fiji</option>
|
||||
<option value="FI">Finland</option>
|
||||
<option value="GF">French Guiana</option>
|
||||
<option value="PF">French Polynesia</option>
|
||||
<option value="TF">French Southern Territories</option>
|
||||
<option value="GA">Gabon</option>
|
||||
<option value="GM">Gambia</option>
|
||||
<option value="GE">Georgia</option>
|
||||
<option value="GH">Ghana</option>
|
||||
<option value="GI">Gibraltar</option>
|
||||
<option value="GR">Greece</option>
|
||||
<option value="GL">Greenland</option>
|
||||
<option value="GD">Grenada</option>
|
||||
<option value="GP">Guadeloupe</option>
|
||||
<option value="GU">Guam</option>
|
||||
<option value="GT">Guatemala</option>
|
||||
<option value="GG">Guernsey</option>
|
||||
<option value="GN">Guinea</option>
|
||||
<option value="GW">Guinea-Bissau</option>
|
||||
<option value="GY">Guyana</option>
|
||||
<option value="HT">Haiti</option>
|
||||
<option value="HM">Heard Island and McDonald Islands</option>
|
||||
<option value="VA">Holy See (Vatican City State)</option>
|
||||
<option value="HN">Honduras</option>
|
||||
<option value="HK">Hong Kong</option>
|
||||
<option value="HU">Hungary</option>
|
||||
<option value="IS">Iceland</option>
|
||||
<option value="ID">Indonesia</option>
|
||||
<option value="IR">Iran, Islamic Republic of</option>
|
||||
<option value="IQ">Iraq</option>
|
||||
<option value="IE">Ireland</option>
|
||||
<option value="IM">Isle of Man</option>
|
||||
<option value="IL">Israel</option>
|
||||
<option value="IT">Italy</option>
|
||||
<option value="JM">Jamaica</option>
|
||||
<option value="JP">Japan</option>
|
||||
<option value="JE">Jersey</option>
|
||||
<option value="JO">Jordan</option>
|
||||
<option value="KZ">Kazakhstan</option>
|
||||
<option value="KE">Kenya</option>
|
||||
<option value="KI">Kiribati</option>
|
||||
<option value="KP">Korea, Democratic People's Republic of</option>
|
||||
<option value="KR">Korea, Republic of</option>
|
||||
<option value="KW">Kuwait</option>
|
||||
<option value="KG">Kyrgyzstan</option>
|
||||
<option value="LA">Lao People's Democratic Republic</option>
|
||||
<option value="LV">Latvia</option>
|
||||
<option value="LB">Lebanon</option>
|
||||
<option value="LS">Lesotho</option>
|
||||
<option value="LR">Liberia</option>
|
||||
<option value="LY">Libya</option>
|
||||
<option value="LI">Liechtenstein</option>
|
||||
<option value="LT">Lithuania</option>
|
||||
<option value="LU">Luxembourg</option>
|
||||
<option value="MO">Macao</option>
|
||||
<option value="MK">Macedonia, the former Yugoslav Republic of</option>
|
||||
<option value="MG">Madagascar</option>
|
||||
<option value="MW">Malawi</option>
|
||||
<option value="MY">Malaysia</option>
|
||||
<option value="MV">Maldives</option>
|
||||
<option value="ML">Mali</option>
|
||||
<option value="MT">Malta</option>
|
||||
<option value="MH">Marshall Islands</option>
|
||||
<option value="MQ">Martinique</option>
|
||||
<option value="MR">Mauritania</option>
|
||||
<option value="MU">Mauritius</option>
|
||||
<option value="YT">Mayotte</option>
|
||||
<option value="MX">Mexico</option>
|
||||
<option value="FM">Micronesia, Federated States of</option>
|
||||
<option value="MD">Moldova, Republic of</option>
|
||||
<option value="MC">Monaco</option>
|
||||
<option value="MN">Mongolia</option>
|
||||
<option value="ME">Montenegro</option>
|
||||
<option value="MS">Montserrat</option>
|
||||
<option value="MA">Morocco</option>
|
||||
<option value="MZ">Mozambique</option>
|
||||
<option value="MM">Myanmar</option>
|
||||
<option value="NA">Namibia</option>
|
||||
<option value="NR">Nauru</option>
|
||||
<option value="NP">Nepal</option>
|
||||
<option value="NL">Netherlands</option>
|
||||
<option value="NC">New Caledonia</option>
|
||||
<option value="NZ">New Zealand</option>
|
||||
<option value="NI">Nicaragua</option>
|
||||
<option value="NE">Niger</option>
|
||||
<option value="NG">Nigeria</option>
|
||||
<option value="NU">Niue</option>
|
||||
<option value="NF">Norfolk Island</option>
|
||||
<option value="MP">Northern Mariana Islands</option>
|
||||
<option value="NO">Norway</option>
|
||||
<option value="OM">Oman</option>
|
||||
<option value="PK">Pakistan</option>
|
||||
<option value="PW">Palau</option>
|
||||
<option value="PS">Palestinian Territory, Occupied</option>
|
||||
<option value="PA">Panama</option>
|
||||
<option value="PG">Papua New Guinea</option>
|
||||
<option value="PY">Paraguay</option>
|
||||
<option value="PE">Peru</option>
|
||||
<option value="PH">Philippines</option>
|
||||
<option value="PN">Pitcairn</option>
|
||||
<option value="PL">Poland</option>
|
||||
<option value="PT">Portugal</option>
|
||||
<option value="PR">Puerto Rico</option>
|
||||
<option value="QA">Qatar</option>
|
||||
<option value="RE">Réunion</option>
|
||||
<option value="RO">Romania</option>
|
||||
<option value="RU">Russian Federation</option>
|
||||
<option value="RW">Rwanda</option>
|
||||
<option value="BL">Saint Barthélemy</option>
|
||||
<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
|
||||
<option value="KN">Saint Kitts and Nevis</option>
|
||||
<option value="LC">Saint Lucia</option>
|
||||
<option value="MF">Saint Martin (French part)</option>
|
||||
<option value="PM">Saint Pierre and Miquelon</option>
|
||||
<option value="VC">Saint Vincent and the Grenadines</option>
|
||||
<option value="WS">Samoa</option>
|
||||
<option value="SM">San Marino</option>
|
||||
<option value="ST">Sao Tome and Principe</option>
|
||||
<option value="SA">Saudi Arabia</option>
|
||||
<option value="SN">Senegal</option>
|
||||
<option value="RS">Serbia</option>
|
||||
<option value="SC">Seychelles</option>
|
||||
<option value="SL">Sierra Leone</option>
|
||||
<option value="SG">Singapore</option>
|
||||
<option value="SX">Sint Maarten (Dutch part)</option>
|
||||
<option value="SK">Slovakia</option>
|
||||
<option value="SI">Slovenia</option>
|
||||
<option value="SB">Solomon Islands</option>
|
||||
<option value="SO">Somalia</option>
|
||||
<option value="ZA">South Africa</option>
|
||||
<option value="GS">South Georgia and the South Sandwich Islands</option>
|
||||
<option value="SS">South Sudan</option>
|
||||
<option value="ES">Spain</option>
|
||||
<option value="LK">Sri Lanka</option>
|
||||
<option value="SD">Sudan</option>
|
||||
<option value="SR">Suriname</option>
|
||||
<option value="SJ">Svalbard and Jan Mayen</option>
|
||||
<option value="SZ">Swaziland</option>
|
||||
<option value="SE">Sweden</option>
|
||||
<option value="CH">Switzerland</option>
|
||||
<option value="SY">Syrian Arab Republic</option>
|
||||
<option value="TW">Taiwan</option>
|
||||
<option value="TJ">Tajikistan</option>
|
||||
<option value="TZ">Tanzania, United Republic of</option>
|
||||
<option value="TH">Thailand</option>
|
||||
<option value="TL">Timor-Leste</option>
|
||||
<option value="TG">Togo</option>
|
||||
<option value="TK">Tokelau</option>
|
||||
<option value="TO">Tonga</option>
|
||||
<option value="TT">Trinidad and Tobago</option>
|
||||
<option value="TN">Tunisia</option>
|
||||
<option value="TR">Turkey</option>
|
||||
<option value="TM">Turkmenistan</option>
|
||||
<option value="TC">Turks and Caicos Islands</option>
|
||||
<option value="TV">Tuvalu</option>
|
||||
<option value="UG">Uganda</option>
|
||||
<option value="UA">Ukraine</option>
|
||||
<option value="AE">United Arab Emirates</option>
|
||||
<option value="UM">United States Minor Outlying Islands</option>
|
||||
<option value="UY">Uruguay</option>
|
||||
<option value="UZ">Uzbekistan</option>
|
||||
<option value="VU">Vanuatu</option>
|
||||
<option value="VE">Venezuela, Bolivarian Republic of</option>
|
||||
<option value="VN">Viet Nam</option>
|
||||
<option value="VG">Virgin Islands, British</option>
|
||||
<option value="VI">Virgin Islands, U.S.</option>
|
||||
<option value="WF">Wallis and Futuna</option>
|
||||
<option value="EH">Western Sahara</option>
|
||||
<option value="YE">Yemen</option>
|
||||
<option value="ZM">Zambia</option>
|
||||
<option value="ZW">Zimbabwe</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="State"></label>
|
||||
<select asp-for="State" class="form-control">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="AL">Alabama</option>
|
||||
<option value="AK">Alaska</option>
|
||||
<option value="AZ">Arizona</option>
|
||||
<option value="AR">Arkansas</option>
|
||||
<option value="CA">California</option>
|
||||
<option value="CO">Colorado</option>
|
||||
<option value="CT">Connecticut</option>
|
||||
<option value="DE">Delaware</option>
|
||||
<option value="DC">District Of Columbia</option>
|
||||
<option value="FL">Florida</option>
|
||||
<option value="GA">Georgia</option>
|
||||
<option value="HI">Hawaii</option>
|
||||
<option value="ID">Idaho</option>
|
||||
<option value="IL">Illinois</option>
|
||||
<option value="IN">Indiana</option>
|
||||
<option value="IA">Iowa</option>
|
||||
<option value="KS">Kansas</option>
|
||||
<option value="KY">Kentucky</option>
|
||||
<option value="LA">Louisiana</option>
|
||||
<option value="ME">Maine</option>
|
||||
<option value="MD">Maryland</option>
|
||||
<option value="MA">Massachusetts</option>
|
||||
<option value="MI">Michigan</option>
|
||||
<option value="MN">Minnesota</option>
|
||||
<option value="MS">Mississippi</option>
|
||||
<option value="MO">Missouri</option>
|
||||
<option value="MT">Montana</option>
|
||||
<option value="NE">Nebraska</option>
|
||||
<option value="NV">Nevada</option>
|
||||
<option value="NH">New Hampshire</option>
|
||||
<option value="NJ">New Jersey</option>
|
||||
<option value="NM">New Mexico</option>
|
||||
<option value="NY">New York</option>
|
||||
<option value="NC">North Carolina</option>
|
||||
<option value="ND">North Dakota</option>
|
||||
<option value="OH">Ohio</option>
|
||||
<option value="OK">Oklahoma</option>
|
||||
<option value="OR">Oregon</option>
|
||||
<option value="PA">Pennsylvania</option>
|
||||
<option value="RI">Rhode Island</option>
|
||||
<option value="SC">South Carolina</option>
|
||||
<option value="SD">South Dakota</option>
|
||||
<option value="TN">Tennessee</option>
|
||||
<option value="TX">Texas</option>
|
||||
<option value="UT">Utah</option>
|
||||
<option value="VT">Vermont</option>
|
||||
<option value="VA">Virginia</option>
|
||||
<option value="WA">Washington</option>
|
||||
<option value="WV">West Virginia</option>
|
||||
<option value="WI">Wisconsin</option>
|
||||
<option value="WY">Wyoming</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="PostalCode">Postal Code</label>
|
||||
<input type="text" class="form-control" asp-for="PostalCode" required maxlength="10">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="form-group">
|
||||
<label asp-for="Rate">Tax Rate</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="Rate" pattern="^\d{0,3}.\d{0,3}$" required>
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mb-2">@(string.IsNullOrWhiteSpace(Model.StripeTaxRateId) ? "Create" : "Save")</button>
|
||||
</form>
|
@ -1,11 +1,16 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
@inject Bit.Core.Settings.GlobalSettings GlobalSettings
|
||||
@inject IWebHostEnvironment HostingEnvironment
|
||||
@model UserEditModel
|
||||
@{
|
||||
ViewData["Title"] = "User: " + Model.User.Email;
|
||||
|
||||
var canViewUserInformation = AccessControlService.UserHasPermission(Permission.User_UserInformation_View);
|
||||
var canViewNewDeviceException = AccessControlService.UserHasPermission(Permission.User_NewDeviceException_Edit) &&
|
||||
GlobalSettings.EnableNewDeviceVerification &&
|
||||
FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.NewDeviceVerification);
|
||||
var canViewBillingInformation = AccessControlService.UserHasPermission(Permission.User_BillingInformation_View);
|
||||
var canViewGeneral = AccessControlService.UserHasPermission(Permission.User_GeneralDetails_View);
|
||||
var canViewPremium = AccessControlService.UserHasPermission(Permission.User_Premium_View);
|
||||
@ -47,13 +52,13 @@
|
||||
|
||||
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
|
||||
const url = '@(HostingEnvironment.IsDevelopment()
|
||||
? "https://dashboard.stripe.com/test"
|
||||
: "https://dashboard.stripe.com")';
|
||||
? "https://dashboard.stripe.com/test"
|
||||
: "https://dashboard.stripe.com")';
|
||||
window.open(`${url}/customers/${customerId.value}/`, '_blank');
|
||||
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
|
||||
const url = '@(HostingEnvironment.IsDevelopment()
|
||||
? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}"
|
||||
: $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")';
|
||||
? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}"
|
||||
: $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")';
|
||||
window.open(`${url}/${customerId.value}`, '_blank');
|
||||
}
|
||||
});
|
||||
@ -67,13 +72,13 @@
|
||||
|
||||
if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Stripe)') {
|
||||
const url = '@(HostingEnvironment.IsDevelopment() || HostingEnvironment.IsEnvironment("QA")
|
||||
? "https://dashboard.stripe.com/test"
|
||||
: "https://dashboard.stripe.com")'
|
||||
? "https://dashboard.stripe.com/test"
|
||||
: "https://dashboard.stripe.com")'
|
||||
window.open(`${url}/subscriptions/${subId.value}`, '_blank');
|
||||
} else if (gateway.value === '@((byte)Bit.Core.Enums.GatewayType.Braintree)') {
|
||||
const url = '@(HostingEnvironment.IsDevelopment() || HostingEnvironment.IsEnvironment("QA")
|
||||
? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}"
|
||||
: $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")';
|
||||
? $"https://www.sandbox.braintreegateway.com/merchants/{Model.BraintreeMerchantId}"
|
||||
: $"https://www.braintreegateway.com/merchants/{Model.BraintreeMerchantId}")';
|
||||
window.open(`${url}/subscriptions/${subId.value}`, '_blank');
|
||||
}
|
||||
});
|
||||
@ -88,11 +93,40 @@
|
||||
<h2>User Information</h2>
|
||||
@await Html.PartialAsync("_ViewInformation", Model.User)
|
||||
}
|
||||
@if (canViewNewDeviceException)
|
||||
{
|
||||
<h2>New Device Verification </h2>
|
||||
<dl class="row">
|
||||
<dt class="col d-flex">
|
||||
<form asp-action="ToggleNewDeviceVerification" asp-route-id="@Model.User.Id" method="post">
|
||||
@if (Model.ActiveNewDeviceVerificationException)
|
||||
{
|
||||
<p>Status: Bypassed</p>
|
||||
<button type="submit" class="btn btn-success" id="new-device-verification-exception">Require New
|
||||
Device Verification</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Status: Required</p>
|
||||
<button type="submit" class="btn btn-outline-danger" id="new-device-verification-exception">Bypass New
|
||||
Device Verification</button>
|
||||
}
|
||||
</form>
|
||||
|
||||
</dt>
|
||||
</dl>
|
||||
}
|
||||
@if (canViewBillingInformation)
|
||||
{
|
||||
<h2>Billing Information</h2>
|
||||
@await Html.PartialAsync("_BillingInformation",
|
||||
new BillingInformationModel { BillingInfo = Model.BillingInfo, BillingHistoryInfo = Model.BillingHistoryInfo, UserId = Model.User.Id, Entity = "User" })
|
||||
new BillingInformationModel
|
||||
{
|
||||
BillingInfo = Model.BillingInfo,
|
||||
BillingHistoryInfo = Model.BillingHistoryInfo,
|
||||
UserId = Model.User.Id,
|
||||
Entity = "User"
|
||||
})
|
||||
}
|
||||
@if (canViewGeneral)
|
||||
{
|
||||
@ -109,7 +143,7 @@
|
||||
<label class="form-check-label" asp-for="EmailVerified"></label>
|
||||
</div>
|
||||
}
|
||||
<form method="post" id="edit-form">
|
||||
<form method="post" id="edit-form">
|
||||
@if (canViewPremium)
|
||||
{
|
||||
<h2>Premium</h2>
|
||||
@ -139,54 +173,56 @@
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label asp-for="PremiumExpirationDate" class="form-label"></label>
|
||||
<input type="datetime-local" class="form-control" asp-for="PremiumExpirationDate" readonly='@(!canEditLicensing)'>
|
||||
<input type="datetime-local" class="form-control" asp-for="PremiumExpirationDate"
|
||||
readonly='@(!canEditLicensing)'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (canViewBilling)
|
||||
{
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="Gateway" class="form-label"></label>
|
||||
<select class="form-select" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
||||
@if (canViewBilling)
|
||||
{
|
||||
<h2>Billing</h2>
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="Gateway" class="form-label"></label>
|
||||
<select class="form-select" asp-for="Gateway" disabled='@(canEditBilling ? null : "disabled")'
|
||||
asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewayCustomerId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewayCustomerId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId" readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<button class="btn btn-secondary" type="button" id="gateway-customer-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewaySubscriptionId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId"
|
||||
readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<div class="mb-3">
|
||||
<label asp-for="GatewaySubscriptionId" class="form-label"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId" readonly='@(!canEditBilling)'>
|
||||
@if (canLaunchGateway)
|
||||
{
|
||||
<button class="btn btn-secondary" type="button" id="gateway-subscription-link">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</form>
|
||||
<div class="d-flex mt-4">
|
||||
<button type="submit" class="btn btn-primary" form="edit-form">Save</button>
|
||||
|
120
src/Admin/package-lock.json
generated
120
src/Admin/package-lock.json
generated
@ -780,9 +780,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.2",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
|
||||
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
|
||||
"version": "4.24.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
|
||||
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -800,9 +800,9 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001669",
|
||||
"electron-to-chromium": "^1.5.41",
|
||||
"node-releases": "^2.0.18",
|
||||
"caniuse-lite": "^1.0.30001688",
|
||||
"electron-to-chromium": "^1.5.73",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
@ -820,9 +820,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001688",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz",
|
||||
"integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==",
|
||||
"version": "1.0.30001690",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
|
||||
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -841,9 +841,9 @@
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
|
||||
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -973,16 +973,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.73",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz",
|
||||
"integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==",
|
||||
"version": "1.5.75",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
|
||||
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.17.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
|
||||
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
|
||||
"version": "5.18.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
|
||||
"integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1272,9 +1272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
|
||||
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -1793,19 +1793,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-core-module": "^2.13.0",
|
||||
"is-core-module": "^2.16.0",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@ -2083,17 +2086,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.10",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
|
||||
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
|
||||
"version": "5.3.11",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
|
||||
"integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.20",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jest-worker": "^27.4.5",
|
||||
"schema-utils": "^3.1.1",
|
||||
"serialize-javascript": "^6.0.1",
|
||||
"terser": "^5.26.0"
|
||||
"schema-utils": "^4.3.0",
|
||||
"serialize-javascript": "^6.0.2",
|
||||
"terser": "^5.31.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
@ -2117,59 +2120,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ajv": "^6.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
"ajv-keywords": "^3.5.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
@ -2,7 +2,6 @@
|
||||
using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||
@ -90,7 +89,7 @@ public class GroupsController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroups(Guid orgId)
|
||||
public async Task<ListResponseModel<GroupResponseModel>> GetOrganizationGroups(Guid orgId)
|
||||
{
|
||||
var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll);
|
||||
if (!authResult.Succeeded)
|
||||
@ -98,24 +97,15 @@ public class GroupsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails))
|
||||
{
|
||||
var groups = await _groupRepository.GetManyByOrganizationIdAsync(orgId);
|
||||
var responses = groups.Select(g => new GroupDetailsResponseModel(g, []));
|
||||
return new ListResponseModel<GroupDetailsResponseModel>(responses);
|
||||
}
|
||||
|
||||
var groupDetails = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId);
|
||||
var detailResponses = groupDetails.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2));
|
||||
return new ListResponseModel<GroupDetailsResponseModel>(detailResponses);
|
||||
var groups = await _groupRepository.GetManyByOrganizationIdAsync(orgId);
|
||||
var responses = groups.Select(g => new GroupResponseModel(g));
|
||||
return new ListResponseModel<GroupResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("details")]
|
||||
public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroupDetails(Guid orgId)
|
||||
{
|
||||
var authResult = _featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails)
|
||||
? await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAllDetails)
|
||||
: await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll);
|
||||
var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAllDetails);
|
||||
|
||||
if (!authResult.Succeeded)
|
||||
{
|
||||
|
@ -311,10 +311,8 @@ public class OrganizationUsersController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
var useMasterPasswordPolicy = masterPasswordPolicy != null &&
|
||||
masterPasswordPolicy.Enabled &&
|
||||
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
|
||||
var useMasterPasswordPolicy = await ShouldHandleResetPasswordAsync(orgId);
|
||||
|
||||
if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey))
|
||||
{
|
||||
throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided.");
|
||||
@ -328,6 +326,23 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ShouldHandleResetPasswordAsync(Guid orgId)
|
||||
{
|
||||
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
|
||||
|
||||
if (organizationAbility is not { UsePolicies: true })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
|
||||
var useMasterPasswordPolicy = masterPasswordPolicy != null &&
|
||||
masterPasswordPolicy.Enabled &&
|
||||
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
|
||||
|
||||
return useMasterPasswordPolicy;
|
||||
}
|
||||
|
||||
[HttpPost("{id}/confirm")]
|
||||
public async Task Confirm(string orgId, string id, [FromBody] OrganizationUserConfirmRequestModel model)
|
||||
{
|
||||
|
@ -14,6 +14,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@ -58,6 +59,7 @@ public class OrganizationsController : Controller
|
||||
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly ICloudOrganizationSignUpCommand _cloudOrganizationSignUpCommand;
|
||||
private readonly IOrganizationDeleteCommand _organizationDeleteCommand;
|
||||
|
||||
public OrganizationsController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -78,7 +80,8 @@ public class OrganizationsController : Controller
|
||||
IProviderBillingService providerBillingService,
|
||||
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand)
|
||||
ICloudOrganizationSignUpCommand cloudOrganizationSignUpCommand,
|
||||
IOrganizationDeleteCommand organizationDeleteCommand)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
@ -99,6 +102,7 @@ public class OrganizationsController : Controller
|
||||
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_cloudOrganizationSignUpCommand = cloudOrganizationSignUpCommand;
|
||||
_organizationDeleteCommand = organizationDeleteCommand;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@ -303,7 +307,7 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
await _organizationService.DeleteAsync(organization);
|
||||
await _organizationDeleteCommand.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete-recover-token")]
|
||||
@ -333,7 +337,7 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
await _organizationService.DeleteAsync(organization);
|
||||
await _organizationDeleteCommand.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/api-key")]
|
||||
|
@ -1,4 +1,6 @@
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Controllers;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
@ -7,13 +9,15 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers;
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[Route("providers/{providerId:guid}/clients")]
|
||||
public class ProviderClientsController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ILogger<BaseProviderController> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderBillingService providerBillingService,
|
||||
@ -22,7 +26,10 @@ public class ProviderClientsController(
|
||||
IProviderService providerService,
|
||||
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
||||
{
|
||||
private readonly ICurrentContext _currentContext = currentContext;
|
||||
|
||||
[HttpPost]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IResult> CreateAsync(
|
||||
[FromRoute] Guid providerId,
|
||||
[FromBody] CreateClientOrganizationRequestBody requestBody)
|
||||
@ -80,6 +87,7 @@ public class ProviderClientsController(
|
||||
}
|
||||
|
||||
[HttpPut("{providerOrganizationId:guid}")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IResult> UpdateAsync(
|
||||
[FromRoute] Guid providerId,
|
||||
[FromRoute] Guid providerOrganizationId,
|
||||
@ -113,7 +121,7 @@ public class ProviderClientsController(
|
||||
clientOrganization.PlanType,
|
||||
seatAdjustment);
|
||||
|
||||
if (seatAdjustmentResultsInPurchase && !currentContext.ProviderProviderAdmin(provider.Id))
|
||||
if (seatAdjustmentResultsInPurchase && !_currentContext.ProviderProviderAdmin(provider.Id))
|
||||
{
|
||||
return Error.Unauthorized("Service users cannot purchase additional seats.");
|
||||
}
|
||||
@ -127,4 +135,58 @@ public class ProviderClientsController(
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpGet("addable")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IResult> GetAddableOrganizationsAsync([FromRoute] Guid providerId)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var userId = _currentContext.UserId;
|
||||
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var addable =
|
||||
await providerBillingService.GetAddableOrganizations(provider, userId.Value);
|
||||
|
||||
return TypedResults.Ok(addable);
|
||||
}
|
||||
|
||||
[HttpPost("existing")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IResult> AddExistingOrganizationAsync(
|
||||
[FromRoute] Guid providerId,
|
||||
[FromBody] AddExistingOrganizationRequestBody requestBody)
|
||||
{
|
||||
var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(requestBody.OrganizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.BadRequest("The organization being added to the provider does not exist.");
|
||||
}
|
||||
|
||||
await providerBillingService.AddExistingOrganization(provider, organization, requestBody.Key);
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
}
|
@ -57,6 +57,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
}
|
||||
@ -102,6 +103,7 @@ public class OrganizationResponseModel : ResponseModel
|
||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
public bool UseRiskInsights { get; set; }
|
||||
}
|
||||
|
@ -67,6 +67,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
AccessSecretsManager = organization.AccessSecretsManager;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
@ -128,6 +129,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public bool AccessSecretsManager { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
/// <summary>
|
||||
/// Indicates if the organization manages the user.
|
||||
|
@ -47,6 +47,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
||||
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
LimitItemDeletion = organization.LimitItemDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ public class EventsController : Controller
|
||||
/// If no filters are provided, it will return the last 30 days of event for the organization.
|
||||
/// </remarks>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ListResponseModel<EventResponseModel>), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(PagedListResponseModel<EventResponseModel>), (int)HttpStatusCode.OK)]
|
||||
public async Task<IActionResult> List([FromQuery] EventFilterRequestModel request)
|
||||
{
|
||||
var dateRange = request.ToDateRange();
|
||||
@ -65,7 +65,7 @@ public class EventsController : Controller
|
||||
}
|
||||
|
||||
var eventResponses = result.Data.Select(e => new EventResponseModel(e));
|
||||
var response = new ListResponseModel<EventResponseModel>(eventResponses, result.ContinuationToken);
|
||||
var response = new PagedListResponseModel<EventResponseModel>(eventResponses, result.ContinuationToken);
|
||||
return new JsonResult(response);
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ public class PoliciesController : Controller
|
||||
/// </remarks>
|
||||
/// <param name="type">The type of policy to be retrieved.</param>
|
||||
[HttpGet("{type}")]
|
||||
[ProducesResponseType(typeof(GroupResponseModel), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType(typeof(PolicyResponseModel), (int)HttpStatusCode.OK)]
|
||||
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||
public async Task<IActionResult> Get(PolicyType type)
|
||||
{
|
||||
|
@ -50,6 +50,7 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||
Status = user.Status;
|
||||
Collections = collections?.Select(c => new AssociationWithPermissionsResponseModel(c));
|
||||
ResetPasswordEnrolled = user.ResetPasswordKey != null;
|
||||
SsoExternalId = user.SsoExternalId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -104,4 +105,10 @@ public class MemberResponseModel : MemberBaseModel, IResponseModel
|
||||
/// </summary>
|
||||
[Required]
|
||||
public bool ResetPasswordEnrolled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// SSO external identifier for linking this member to an identity provider.
|
||||
/// </summary>
|
||||
/// <example>sso_external_id_123456</example>
|
||||
public string SsoExternalId { get; set; }
|
||||
}
|
||||
|
@ -4,6 +4,8 @@
|
||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
<ANCMPreConfiguredForIIS>true</ANCMPreConfiguredForIIS>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS8604</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
@ -266,8 +266,18 @@ public class AccountsController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
user = model.ToUser(user);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, e.Message);
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
var result = await _setInitialMasterPasswordCommand.SetInitialMasterPasswordAsync(
|
||||
model.ToUser(user),
|
||||
user,
|
||||
model.MasterPasswordHash,
|
||||
model.Key,
|
||||
model.OrgIdentifier);
|
||||
@ -969,11 +979,27 @@ public class AccountsController : Controller
|
||||
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
|
||||
[AllowAnonymous]
|
||||
[HttpPost("resend-new-device-otp")]
|
||||
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificatioRequestModel request)
|
||||
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
|
||||
{
|
||||
await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret);
|
||||
}
|
||||
|
||||
[HttpPost("verify-devices")]
|
||||
[HttpPut("verify-devices")]
|
||||
public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException();
|
||||
|
||||
if (!await _userService.VerifySecretAsync(user, request.Secret))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||
}
|
||||
user.VerifyDevices = request.VerifyDevices;
|
||||
|
||||
await _userService.SaveUserAsync(user);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Guid>> GetOrganizationIdsManagingUserAsync(Guid userId)
|
||||
{
|
||||
var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId);
|
||||
|
@ -0,0 +1,9 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
||||
|
||||
public class SetVerifyDevicesRequestModel : SecretVerificationRequestModel
|
||||
{
|
||||
[Required]
|
||||
public bool VerifyDevices { get; set; }
|
||||
}
|
@ -3,7 +3,7 @@ using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
||||
|
||||
public class UnauthenticatedSecretVerificatioRequestModel : SecretVerificationRequestModel
|
||||
public class UnauthenticatedSecretVerificationRequestModel : SecretVerificationRequestModel
|
||||
{
|
||||
[Required]
|
||||
[StrictEmailAddress]
|
@ -1,6 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Response;
|
||||
@ -17,6 +18,7 @@ public class AuthRequestResponseModel : ResponseModel
|
||||
|
||||
Id = authRequest.Id;
|
||||
PublicKey = authRequest.PublicKey;
|
||||
RequestDeviceTypeValue = authRequest.RequestDeviceType;
|
||||
RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
||||
RequestIpAddress = authRequest.RequestIpAddress;
|
||||
@ -30,6 +32,7 @@ public class AuthRequestResponseModel : ResponseModel
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string PublicKey { get; set; }
|
||||
public DeviceType RequestDeviceTypeValue { get; set; }
|
||||
public string RequestDeviceType { get; set; }
|
||||
public string RequestIpAddress { get; set; }
|
||||
public string Key { get; set; }
|
||||
|
@ -1,7 +1,9 @@
|
||||
#nullable enable
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Models.Sales;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Repositories;
|
||||
@ -16,12 +18,12 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class OrganizationBillingController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IPaymentService paymentService,
|
||||
ISubscriberService subscriberService,
|
||||
IPaymentHistoryService paymentHistoryService) : BaseBillingController
|
||||
IPaymentHistoryService paymentHistoryService,
|
||||
IUserService userService) : BaseBillingController
|
||||
{
|
||||
[HttpGet("metadata")]
|
||||
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
||||
@ -136,11 +138,6 @@ public class OrganizationBillingController(
|
||||
[HttpGet("payment-method")]
|
||||
public async Task<IResult> GetPaymentMethodAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
@ -165,11 +162,6 @@ public class OrganizationBillingController(
|
||||
[FromRoute] Guid organizationId,
|
||||
[FromBody] UpdatePaymentMethodRequestBody requestBody)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
@ -196,11 +188,6 @@ public class OrganizationBillingController(
|
||||
[FromRoute] Guid organizationId,
|
||||
[FromBody] VerifyBankAccountRequestBody requestBody)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
@ -226,11 +213,6 @@ public class OrganizationBillingController(
|
||||
[HttpGet("tax-information")]
|
||||
public async Task<IResult> GetTaxInformationAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
@ -255,11 +237,6 @@ public class OrganizationBillingController(
|
||||
[FromRoute] Guid organizationId,
|
||||
[FromBody] TaxInformationRequestBody requestBody)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
@ -278,4 +255,43 @@ public class OrganizationBillingController(
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
|
||||
[HttpPost("restart-subscription")]
|
||||
public async Task<IResult> RestartSubscriptionAsync([FromRoute] Guid organizationId,
|
||||
[FromBody] OrganizationCreateRequestModel model)
|
||||
{
|
||||
var user = await userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
var organizationSignup = model.ToOrganizationSignup(user);
|
||||
var sale = OrganizationSale.From(organization, organizationSignup);
|
||||
var plan = StaticStore.GetPlan(model.PlanType);
|
||||
sale.Organization.PlanType = plan.Type;
|
||||
sale.Organization.Plan = plan.Name;
|
||||
sale.SubscriptionSetup.SkipTrial = true;
|
||||
await organizationBillingService.Finalize(sale);
|
||||
var org = await organizationRepository.GetByIdAsync(organizationId);
|
||||
if (organizationSignup.PaymentMethodType != null)
|
||||
{
|
||||
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
|
||||
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
|
||||
await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation);
|
||||
}
|
||||
|
||||
return TypedResults.Ok();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response.Organizations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
@ -107,7 +106,7 @@ public class OrganizationSponsorshipsController : Controller
|
||||
{
|
||||
var isFreeFamilyPolicyEnabled = false;
|
||||
var (isValid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email);
|
||||
if (isValid && _featureService.IsEnabled(FeatureFlagKeys.DisableFreeFamiliesSponsorship) && sponsorship.SponsoringOrganizationId.HasValue)
|
||||
if (isValid && sponsorship.SponsoringOrganizationId.HasValue)
|
||||
{
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsorship.SponsoringOrganizationId.Value,
|
||||
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||
|
@ -0,0 +1,12 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests;
|
||||
|
||||
public class AddExistingOrganizationRequestBody
|
||||
{
|
||||
[Required(ErrorMessage = "'key' must be provided")]
|
||||
public string Key { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "'organizationId' must be provided")]
|
||||
public Guid OrganizationId { get; set; }
|
||||
}
|
@ -9,6 +9,7 @@ public record OrganizationMetadataResponse(
|
||||
bool IsSubscriptionUnpaid,
|
||||
bool HasSubscription,
|
||||
bool HasOpenInvoice,
|
||||
bool IsSubscriptionCanceled,
|
||||
DateTime? InvoiceDueDate,
|
||||
DateTime? InvoiceCreatedDate,
|
||||
DateTime? SubPeriodEndDate)
|
||||
@ -21,6 +22,7 @@ public record OrganizationMetadataResponse(
|
||||
metadata.IsSubscriptionUnpaid,
|
||||
metadata.HasSubscription,
|
||||
metadata.HasOpenInvoice,
|
||||
metadata.IsSubscriptionCanceled,
|
||||
metadata.InvoiceDueDate,
|
||||
metadata.InvoiceCreatedDate,
|
||||
metadata.SubPeriodEndDate);
|
||||
|
@ -6,7 +6,6 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Core.Auth.Models.Api.Request;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -70,11 +69,17 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<DeviceResponseModel>> Get()
|
||||
public async Task<ListResponseModel<DeviceAuthRequestResponseModel>> Get()
|
||||
{
|
||||
ICollection<Device> devices = await _deviceRepository.GetManyByUserIdAsync(_userService.GetProperUserId(User).Value);
|
||||
var responses = devices.Select(d => new DeviceResponseModel(d));
|
||||
return new ListResponseModel<DeviceResponseModel>(responses);
|
||||
var devicesWithPendingAuthData = await _deviceRepository.GetManyByUserIdWithDeviceAuth(_userService.GetProperUserId(User).Value);
|
||||
|
||||
// Convert from DeviceAuthDetails to DeviceAuthRequestResponseModel
|
||||
var deviceAuthRequestResponseList = devicesWithPendingAuthData
|
||||
.Select(DeviceAuthRequestResponseModel.From)
|
||||
.ToList();
|
||||
|
||||
var response = new ListResponseModel<DeviceAuthRequestResponseModel>(deviceAuthRequestResponseList);
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@ -10,10 +9,6 @@ namespace Bit.Api.Controllers;
|
||||
[Authorize("Web")]
|
||||
public class PlansController : Controller
|
||||
{
|
||||
private readonly ITaxRateRepository _taxRateRepository;
|
||||
|
||||
public PlansController(ITaxRateRepository taxRateRepository) => _taxRateRepository = taxRateRepository;
|
||||
|
||||
[HttpGet("")]
|
||||
[AllowAnonymous]
|
||||
public ListResponseModel<PlanResponseModel> Get()
|
||||
@ -21,12 +16,4 @@ public class PlansController : Controller
|
||||
var responses = StaticStore.Plans.Select(plan => new PlanResponseModel(plan));
|
||||
return new ListResponseModel<PlanResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("sales-tax-rates")]
|
||||
public async Task<ListResponseModel<TaxRateResponseModel>> GetTaxRates()
|
||||
{
|
||||
var data = await _taxRateRepository.GetAllActiveAsync();
|
||||
var responses = data.Select(x => new TaxRateResponseModel(x));
|
||||
return new ListResponseModel<TaxRateResponseModel>(responses);
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,9 @@ namespace Bit.Api.Models.Public.Response;
|
||||
|
||||
public class ListResponseModel<T> : IResponseModel where T : IResponseModel
|
||||
{
|
||||
public ListResponseModel(IEnumerable<T> data, string continuationToken = null)
|
||||
public ListResponseModel(IEnumerable<T> data)
|
||||
{
|
||||
Data = data;
|
||||
ContinuationToken = continuationToken;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -21,8 +20,4 @@ public class ListResponseModel<T> : IResponseModel where T : IResponseModel
|
||||
/// </summary>
|
||||
[Required]
|
||||
public IEnumerable<T> Data { get; set; }
|
||||
/// <summary>
|
||||
/// A cursor for use in pagination.
|
||||
/// </summary>
|
||||
public string ContinuationToken { get; set; }
|
||||
}
|
||||
|
10
src/Api/Models/Public/Response/PagedListResponseModel.cs
Normal file
10
src/Api/Models/Public/Response/PagedListResponseModel.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace Bit.Api.Models.Public.Response;
|
||||
|
||||
public class PagedListResponseModel<T>(IEnumerable<T> data, string continuationToken) : ListResponseModel<T>(data)
|
||||
where T : IResponseModel
|
||||
{
|
||||
/// <summary>
|
||||
/// A cursor for use in pagination.
|
||||
/// </summary>
|
||||
public string ContinuationToken { get; set; } = continuationToken;
|
||||
}
|
@ -7,12 +7,14 @@ public class OrganizationCollectionManagementUpdateRequestModel
|
||||
{
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
|
||||
public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService)
|
||||
{
|
||||
existingOrganization.LimitCollectionCreation = LimitCollectionCreation;
|
||||
existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion;
|
||||
existingOrganization.LimitItemDeletion = LimitItemDeletion;
|
||||
existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems;
|
||||
return existingOrganization;
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ public class ProfileResponseModel : ResponseModel
|
||||
UsesKeyConnector = user.UsesKeyConnector;
|
||||
AvatarColor = user.AvatarColor;
|
||||
CreationDate = user.CreationDate;
|
||||
VerifyDevices = user.VerifyDevices;
|
||||
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingUser));
|
||||
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
|
||||
ProviderOrganizations =
|
||||
@ -62,6 +63,7 @@ public class ProfileResponseModel : ResponseModel
|
||||
public bool UsesKeyConnector { get; set; }
|
||||
public string AvatarColor { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public bool VerifyDevices { get; set; }
|
||||
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
|
||||
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
|
||||
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }
|
||||
|
@ -1,28 +0,0 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Models.Response;
|
||||
|
||||
public class TaxRateResponseModel : ResponseModel
|
||||
{
|
||||
public TaxRateResponseModel(TaxRate taxRate)
|
||||
: base("profile")
|
||||
{
|
||||
if (taxRate == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(taxRate));
|
||||
}
|
||||
|
||||
Id = taxRate.Id;
|
||||
Country = taxRate.Country;
|
||||
State = taxRate.State;
|
||||
PostalCode = taxRate.PostalCode;
|
||||
Rate = taxRate.Rate;
|
||||
}
|
||||
|
||||
public string Id { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string State { get; set; }
|
||||
public string PostalCode { get; set; }
|
||||
public decimal Rate { get; set; }
|
||||
}
|
@ -1,13 +1,20 @@
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
namespace Bit.Api.Platform.Installations;
|
||||
|
||||
/// <summary>
|
||||
/// Routes used to manipulate `Installation` objects: a type used to manage
|
||||
/// a record of a self hosted installation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This controller is not called from any clients. It's primarily referenced
|
||||
/// in the `Setup` project for creating a new self hosted installation.
|
||||
/// </remarks>
|
||||
/// <seealso>Bit.Setup.Program</seealso>
|
||||
[Route("installations")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class InstallationsController : Controller
|
@ -1,8 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.Models.Request;
|
||||
namespace Bit.Api.Platform.Installations;
|
||||
|
||||
public class InstallationRequestModel
|
||||
{
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Platform.Installations;
|
||||
|
||||
namespace Bit.Api.Models.Response;
|
||||
namespace Bit.Api.Platform.Installations;
|
||||
|
||||
public class InstallationResponseModel : ResponseModel
|
||||
{
|
@ -2,14 +2,18 @@
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
namespace Bit.Api.Platform.Push;
|
||||
|
||||
/// <summary>
|
||||
/// Routes for push relay: functionality that facilitates communication
|
||||
/// between self hosted organizations and Bitwarden cloud.
|
||||
/// </summary>
|
||||
[Route("push")]
|
||||
[Authorize("Push")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
@ -29,6 +29,7 @@ using Bit.Core.Vault.Entities;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Tools.ImportFeatures;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
|
||||
|
||||
@ -175,6 +176,7 @@ public class Startup
|
||||
services.AddCoreLocalizationServices();
|
||||
services.AddBillingOperations();
|
||||
services.AddReportingServices();
|
||||
services.AddImportServices();
|
||||
|
||||
// Authorization Handlers
|
||||
services.AddAuthorizationHandlers();
|
||||
|
@ -7,7 +7,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Vault.Services;
|
||||
using Bit.Core.Tools.ImportFeatures.Interfaces;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -17,31 +17,30 @@ namespace Bit.Api.Tools.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class ImportCiphersController : Controller
|
||||
{
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ILogger<ImportCiphersController> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IImportCiphersCommand _importCiphersCommand;
|
||||
|
||||
public ImportCiphersController(
|
||||
ICipherService cipherService,
|
||||
IUserService userService,
|
||||
ICurrentContext currentContext,
|
||||
ILogger<ImportCiphersController> logger,
|
||||
GlobalSettings globalSettings,
|
||||
ICollectionRepository collectionRepository,
|
||||
IAuthorizationService authorizationService,
|
||||
IOrganizationRepository organizationRepository)
|
||||
IImportCiphersCommand importCiphersCommand)
|
||||
{
|
||||
_cipherService = cipherService;
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_collectionRepository = collectionRepository;
|
||||
_authorizationService = authorizationService;
|
||||
_importCiphersCommand = importCiphersCommand;
|
||||
}
|
||||
|
||||
[HttpPost("import")]
|
||||
@ -57,7 +56,7 @@ public class ImportCiphersController : Controller
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList();
|
||||
var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList();
|
||||
await _cipherService.ImportCiphersAsync(folders, ciphers, model.FolderRelationships);
|
||||
await _importCiphersCommand.ImportIntoIndividualVaultAsync(folders, ciphers, model.FolderRelationships);
|
||||
}
|
||||
|
||||
[HttpPost("import-organization")]
|
||||
@ -65,8 +64,9 @@ public class ImportCiphersController : Controller
|
||||
[FromBody] ImportOrganizationCiphersRequestModel model)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted &&
|
||||
(model.Ciphers.Count() > 7000 || model.CollectionRelationships.Count() > 14000 ||
|
||||
model.Collections.Count() > 2000))
|
||||
(model.Ciphers.Count() > _globalSettings.ImportCiphersLimitation.CiphersLimit ||
|
||||
model.CollectionRelationships.Count() > _globalSettings.ImportCiphersLimitation.CollectionRelationshipsLimit ||
|
||||
model.Collections.Count() > _globalSettings.ImportCiphersLimitation.CollectionsLimit))
|
||||
{
|
||||
throw new BadRequestException("You cannot import this much data at once.");
|
||||
}
|
||||
@ -85,7 +85,7 @@ public class ImportCiphersController : Controller
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var ciphers = model.Ciphers.Select(l => l.ToOrganizationCipherDetails(orgId)).ToList();
|
||||
await _cipherService.ImportCiphersAsync(collections, ciphers, model.CollectionRelationships, userId);
|
||||
await _importCiphersCommand.ImportIntoOrganizationalVaultAsync(collections, ciphers, model.CollectionRelationships, userId);
|
||||
}
|
||||
|
||||
private async Task<bool> CheckOrgImportPermission(List<Collection> collections, Guid orgId)
|
||||
|
@ -1,16 +1,11 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Tools.Authorization;
|
||||
using Bit.Api.Tools.Authorization;
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Queries;
|
||||
using Bit.Core.Vault.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -56,39 +51,6 @@ public class OrganizationExportController : Controller
|
||||
|
||||
[HttpGet("export")]
|
||||
public async Task<IActionResult> Export(Guid organizationId)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM11360RemoveProviderExportPermission))
|
||||
{
|
||||
return await Export_vNext(organizationId);
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
||||
IEnumerable<Collection> orgCollections = await _collectionService.GetOrganizationCollectionsAsync(organizationId);
|
||||
(IEnumerable<CipherOrganizationDetails> orgCiphers, Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict) = await _cipherService.GetOrganizationCiphers(userId, organizationId);
|
||||
|
||||
if (_currentContext.ClientVersion == null || _currentContext.ClientVersion >= new Version("2023.1.0"))
|
||||
{
|
||||
var organizationExportResponseModel = new OrganizationExportResponseModel
|
||||
{
|
||||
Collections = orgCollections.Select(c => new CollectionResponseModel(c)),
|
||||
Ciphers = orgCiphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings, collectionCiphersGroupDict, c.OrganizationUseTotp))
|
||||
};
|
||||
|
||||
return Ok(organizationExportResponseModel);
|
||||
}
|
||||
|
||||
// Backward compatibility with versions before 2023.1.0 that use ListResponseModel
|
||||
var organizationExportListResponseModel = new OrganizationExportListResponseModel
|
||||
{
|
||||
Collections = GetOrganizationCollectionsResponse(orgCollections),
|
||||
Ciphers = GetOrganizationCiphersResponse(orgCiphers, collectionCiphersGroupDict)
|
||||
};
|
||||
|
||||
return Ok(organizationExportListResponseModel);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> Export_vNext(Guid organizationId)
|
||||
{
|
||||
var canExportAll = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(organizationId),
|
||||
VaultExportOperations.ExportWholeVault);
|
||||
@ -116,19 +78,4 @@ public class OrganizationExportController : Controller
|
||||
// Unauthorized
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
private ListResponseModel<CollectionResponseModel> GetOrganizationCollectionsResponse(IEnumerable<Collection> orgCollections)
|
||||
{
|
||||
var collections = orgCollections.Select(c => new CollectionResponseModel(c));
|
||||
return new ListResponseModel<CollectionResponseModel>(collections);
|
||||
}
|
||||
|
||||
private ListResponseModel<CipherMiniDetailsResponseModel> GetOrganizationCiphersResponse(IEnumerable<CipherOrganizationDetails> orgCiphers,
|
||||
Dictionary<Guid, IGrouping<Guid, CollectionCipher>> collectionCiphersGroupDict)
|
||||
{
|
||||
var responses = orgCiphers.Select(c => new CipherMiniDetailsResponseModel(c, _globalSettings,
|
||||
collectionCiphersGroupDict, c.OrganizationUseTotp));
|
||||
|
||||
return new ListResponseModel<CipherMiniDetailsResponseModel>(responses);
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Enums;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
@ -163,32 +162,6 @@ public class SendsController : Controller
|
||||
return new SendResponseModel(send, _globalSettings);
|
||||
}
|
||||
|
||||
[HttpPost("file")]
|
||||
[Obsolete("Deprecated File Send API", false)]
|
||||
[RequestSizeLimit(Constants.FileSize101mb)]
|
||||
[DisableFormValueModelBinding]
|
||||
public async Task<SendResponseModel> PostFile()
|
||||
{
|
||||
if (!Request?.ContentType.Contains("multipart/") ?? true)
|
||||
{
|
||||
throw new BadRequestException("Invalid content.");
|
||||
}
|
||||
|
||||
Send send = null;
|
||||
await Request.GetSendFileAsync(async (stream, fileName, model) =>
|
||||
{
|
||||
model.ValidateCreation();
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var (madeSend, madeData) = model.ToSend(userId, fileName, _sendService);
|
||||
send = madeSend;
|
||||
await _sendService.SaveFileSendAsync(send, madeData, model.FileLength.GetValueOrDefault(0));
|
||||
await _sendService.UploadFileToExistingSendAsync(stream, send);
|
||||
});
|
||||
|
||||
return new SendResponseModel(send, _globalSettings);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("file/v2")]
|
||||
public async Task<SendFileUploadDataResponseModel> PostFile([FromBody] SendRequestModel model)
|
||||
{
|
||||
|
@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||
using Bit.SharedWeb.Health;
|
||||
using Bit.SharedWeb.Swagger;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -104,5 +105,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddScoped<IAuthorizationHandler, CollectionAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, GroupAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
|
||||
}
|
||||
}
|
||||
|
@ -424,6 +424,59 @@ public class CiphersController : Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Move this to its own authorization handler or equivalent service - AC-2062
|
||||
/// </summary>
|
||||
private async Task<bool> CanModifyCipherCollectionsAsync(Guid organizationId, IEnumerable<Guid> cipherIds)
|
||||
{
|
||||
// If the user can edit all ciphers for the organization, just check they all belong to the org
|
||||
if (await CanEditAllCiphersAsync(organizationId))
|
||||
{
|
||||
// TODO: This can likely be optimized to only query the requested ciphers and then checking they belong to the org
|
||||
var orgCiphers = (await _cipherRepository.GetManyByOrganizationIdAsync(organizationId)).ToDictionary(c => c.Id);
|
||||
|
||||
// Ensure all requested ciphers are in orgCiphers
|
||||
if (cipherIds.Any(c => !orgCiphers.ContainsKey(c)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// The user cannot access any ciphers for the organization, we're done
|
||||
if (!await CanAccessOrganizationCiphersAsync(organizationId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
// Select all editable ciphers for this user belonging to the organization
|
||||
var editableOrgCipherList = (await _cipherRepository.GetManyByUserIdAsync(userId, true))
|
||||
.Where(c => c.OrganizationId == organizationId && c.UserId == null && c.Edit && c.ViewPassword).ToList();
|
||||
|
||||
// Special case for unassigned ciphers
|
||||
if (await CanAccessUnassignedCiphersAsync(organizationId))
|
||||
{
|
||||
var unassignedCiphers =
|
||||
(await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(
|
||||
organizationId));
|
||||
|
||||
// Users that can access unassigned ciphers can also edit them
|
||||
editableOrgCipherList.AddRange(unassignedCiphers.Select(c => new CipherDetails(c) { Edit = true }));
|
||||
}
|
||||
|
||||
var editableOrgCiphers = editableOrgCipherList
|
||||
.ToDictionary(c => c.Id);
|
||||
|
||||
if (cipherIds.Any(c => !editableOrgCiphers.ContainsKey(c)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TODO: Move this to its own authorization handler or equivalent service - AC-2062
|
||||
/// </summary>
|
||||
@ -579,7 +632,7 @@ public class CiphersController : Controller
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
if (cipher == null || !cipher.OrganizationId.HasValue ||
|
||||
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
|
||||
!await _currentContext.OrganizationUser(cipher.OrganizationId.Value) || !cipher.ViewPassword)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
@ -634,7 +687,7 @@ public class CiphersController : Controller
|
||||
[HttpPost("bulk-collections")]
|
||||
public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model)
|
||||
{
|
||||
if (!await CanEditCiphersAsync(model.OrganizationId, model.CipherIds) ||
|
||||
if (!await CanModifyCipherCollectionsAsync(model.OrganizationId, model.CipherIds) ||
|
||||
!await CanEditItemsInCollections(model.OrganizationId, model.CollectionIds))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -1097,7 +1150,7 @@ public class CiphersController : Controller
|
||||
|
||||
[HttpDelete("{id}/attachment/{attachmentId}")]
|
||||
[HttpPost("{id}/attachment/{attachmentId}/delete")]
|
||||
public async Task DeleteAttachment(Guid id, string attachmentId)
|
||||
public async Task<DeleteAttachmentResponseData> DeleteAttachment(Guid id, string attachmentId)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var cipher = await GetByIdAsync(id, userId);
|
||||
@ -1106,7 +1159,7 @@ public class CiphersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false);
|
||||
return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/attachment/{attachmentId}/admin")]
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Services;
|
||||
@ -19,15 +20,21 @@ public class SecurityTaskController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly IGetTaskDetailsForUserQuery _getTaskDetailsForUserQuery;
|
||||
private readonly IMarkTaskAsCompleteCommand _markTaskAsCompleteCommand;
|
||||
private readonly IGetTasksForOrganizationQuery _getTasksForOrganizationQuery;
|
||||
private readonly ICreateManyTasksCommand _createManyTasksCommand;
|
||||
|
||||
public SecurityTaskController(
|
||||
IUserService userService,
|
||||
IGetTaskDetailsForUserQuery getTaskDetailsForUserQuery,
|
||||
IMarkTaskAsCompleteCommand markTaskAsCompleteCommand)
|
||||
IMarkTaskAsCompleteCommand markTaskAsCompleteCommand,
|
||||
IGetTasksForOrganizationQuery getTasksForOrganizationQuery,
|
||||
ICreateManyTasksCommand createManyTasksCommand)
|
||||
{
|
||||
_userService = userService;
|
||||
_getTaskDetailsForUserQuery = getTaskDetailsForUserQuery;
|
||||
_markTaskAsCompleteCommand = markTaskAsCompleteCommand;
|
||||
_getTasksForOrganizationQuery = getTasksForOrganizationQuery;
|
||||
_createManyTasksCommand = createManyTasksCommand;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -54,4 +61,33 @@ public class SecurityTaskController : Controller
|
||||
await _markTaskAsCompleteCommand.CompleteAsync(taskId);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves security tasks for an organization. Restricted to organization administrators.
|
||||
/// </summary>
|
||||
/// <param name="organizationId">The organization Id</param>
|
||||
/// <param name="status">Optional filter for task status. If not provided, returns tasks of all statuses.</param>
|
||||
[HttpGet("organization")]
|
||||
public async Task<ListResponseModel<SecurityTasksResponseModel>> ListForOrganization(
|
||||
[FromQuery] Guid organizationId, [FromQuery] SecurityTaskStatus? status)
|
||||
{
|
||||
var securityTasks = await _getTasksForOrganizationQuery.GetTasksAsync(organizationId, status);
|
||||
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
|
||||
return new ListResponseModel<SecurityTasksResponseModel>(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulk create security tasks for an organization.
|
||||
/// </summary>
|
||||
/// <param name="orgId"></param>
|
||||
/// <param name="model"></param>
|
||||
/// <returns>A list response model containing the security tasks created for the organization.</returns>
|
||||
[HttpPost("{orgId:guid}/bulk-create")]
|
||||
public async Task<ListResponseModel<SecurityTasksResponseModel>> BulkCreateTasks(Guid orgId,
|
||||
[FromBody] BulkCreateSecurityTasksRequestModel model)
|
||||
{
|
||||
var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks);
|
||||
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
|
||||
return new ListResponseModel<SecurityTasksResponseModel>(response);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Vault.Models.Api;
|
||||
|
||||
namespace Bit.Api.Vault.Models.Request;
|
||||
|
||||
public class BulkCreateSecurityTasksRequestModel
|
||||
{
|
||||
public IEnumerable<SecurityTaskCreateRequest> Tasks { get; set; }
|
||||
}
|
@ -56,6 +56,11 @@
|
||||
"publicKey": "SECRET",
|
||||
"privateKey": "SECRET"
|
||||
},
|
||||
"importCiphersLimitation": {
|
||||
"ciphersLimit": 40000,
|
||||
"collectionRelationshipsLimit": 80000,
|
||||
"collectionsLimit": 2000
|
||||
},
|
||||
"bitPay": {
|
||||
"production": false,
|
||||
"token": "SECRET",
|
||||
|
@ -3,6 +3,8 @@
|
||||
<PropertyGroup>
|
||||
<UserSecretsId>bitwarden-Billing</UserSecretsId>
|
||||
<MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish>
|
||||
<!-- Temp exclusions until warnings are fixed -->
|
||||
<WarningsNotAsErrors>$(WarningsNotAsErrors);CS9113</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(RunConfiguration)' == 'Billing' " />
|
||||
@ -10,5 +12,8 @@
|
||||
<ProjectReference Include="..\SharedWeb\SharedWeb.csproj" />
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -12,6 +12,7 @@ public class BillingSettings
|
||||
public virtual FreshDeskSettings FreshDesk { get; set; } = new FreshDeskSettings();
|
||||
public virtual string FreshsalesApiKey { get; set; }
|
||||
public virtual PayPalSettings PayPal { get; set; } = new PayPalSettings();
|
||||
public virtual OnyxSettings Onyx { get; set; } = new OnyxSettings();
|
||||
|
||||
public class PayPalSettings
|
||||
{
|
||||
@ -31,4 +32,10 @@ public class BillingSettings
|
||||
public virtual string UserFieldName { get; set; }
|
||||
public virtual string OrgFieldName { get; set; }
|
||||
}
|
||||
|
||||
public class OnyxSettings
|
||||
{
|
||||
public virtual string ApiKey { get; set; }
|
||||
public virtual string BaseUrl { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using Microsoft.Extensions.Options;
|
||||
namespace Bit.Billing.Controllers;
|
||||
|
||||
[Route("bitpay")]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class BitPayController : Controller
|
||||
{
|
||||
private readonly BillingSettings _billingSettings;
|
||||
|
@ -1,6 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Web;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Core.Repositories;
|
||||
@ -17,7 +19,6 @@ public class FreshdeskController : Controller
|
||||
private readonly BillingSettings _billingSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ILogger<FreshdeskController> _logger;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
@ -25,7 +26,6 @@ public class FreshdeskController : Controller
|
||||
public FreshdeskController(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOptions<BillingSettings> billingSettings,
|
||||
ILogger<FreshdeskController> logger,
|
||||
GlobalSettings globalSettings,
|
||||
@ -34,7 +34,6 @@ public class FreshdeskController : Controller
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_userRepository = userRepository;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
@ -145,6 +144,121 @@ public class FreshdeskController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("webhook-onyx-ai")]
|
||||
public async Task<IActionResult> PostWebhookOnyxAi([FromQuery, Required] string key,
|
||||
[FromBody, Required] FreshdeskWebhookModel model)
|
||||
{
|
||||
// ensure that the key is from Freshdesk
|
||||
if (!IsValidRequestFromFreshdesk(key))
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
// get ticket info from Freshdesk
|
||||
var getTicketRequest = new HttpRequestMessage(HttpMethod.Get,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}", model.TicketId));
|
||||
var getTicketResponse = await CallFreshdeskApiAsync(getTicketRequest);
|
||||
|
||||
// check if we have a valid response from freshdesk
|
||||
if (getTicketResponse.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Error getting ticket info from Freshdesk. Ticket Id: {0}. Status code: {1}",
|
||||
model.TicketId, getTicketResponse.StatusCode);
|
||||
return BadRequest("Failed to retrieve ticket info from Freshdesk");
|
||||
}
|
||||
|
||||
// extract info from the response
|
||||
var ticketInfo = await ExtractTicketInfoFromResponse(getTicketResponse);
|
||||
if (ticketInfo == null)
|
||||
{
|
||||
return BadRequest("Failed to extract ticket info from Freshdesk response");
|
||||
}
|
||||
|
||||
// create the onyx `answer-with-citation` request
|
||||
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(ticketInfo.DescriptionText);
|
||||
var onyxRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl))
|
||||
{
|
||||
Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")),
|
||||
};
|
||||
var (_, onyxJsonResponse) = await CallOnyxApi<OnyxAnswerWithCitationResponseModel>(onyxRequest);
|
||||
|
||||
// the CallOnyxApi will return a null if we have an error response
|
||||
if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg))
|
||||
{
|
||||
return BadRequest(
|
||||
string.Format("Failed to get a valid response from Onyx API. Response: {0}",
|
||||
JsonSerializer.Serialize(onyxJsonResponse ?? new OnyxAnswerWithCitationResponseModel())));
|
||||
}
|
||||
|
||||
// add the answer as a note to the ticket
|
||||
await AddAnswerNoteToTicketAsync(onyxJsonResponse.Answer, model.TicketId);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private bool IsValidRequestFromFreshdesk(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)
|
||||
|| !CoreHelpers.FixedTimeEquals(key, _billingSettings.FreshDesk.WebhookKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task AddAnswerNoteToTicketAsync(string note, string ticketId)
|
||||
{
|
||||
// if there is no content, then we don't need to add a note
|
||||
if (string.IsNullOrWhiteSpace(note))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var noteBody = new Dictionary<string, object>
|
||||
{
|
||||
{ "body", $"<b>Onyx AI:</b><ul>{note}</ul>" },
|
||||
{ "private", true }
|
||||
};
|
||||
|
||||
var noteRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(noteBody),
|
||||
};
|
||||
|
||||
var addNoteResponse = await CallFreshdeskApiAsync(noteRequest);
|
||||
if (addNoteResponse.StatusCode != System.Net.HttpStatusCode.Created)
|
||||
{
|
||||
_logger.LogError("Error adding note to Freshdesk ticket. Ticket Id: {0}. Status: {1}",
|
||||
ticketId, addNoteResponse.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<FreshdeskViewTicketModel> ExtractTicketInfoFromResponse(HttpResponseMessage getTicketResponse)
|
||||
{
|
||||
var responseString = string.Empty;
|
||||
try
|
||||
{
|
||||
responseString = await getTicketResponse.Content.ReadAsStringAsync();
|
||||
var ticketInfo = JsonSerializer.Deserialize<FreshdeskViewTicketModel>(responseString,
|
||||
options: new System.Text.Json.JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
|
||||
return ticketInfo;
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
_logger.LogError("Error deserializing ticket info from Freshdesk response. Response: {0}. Exception {1}",
|
||||
responseString, ex.ToString());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0)
|
||||
{
|
||||
try
|
||||
@ -169,6 +283,26 @@ public class FreshdeskController : Controller
|
||||
return await CallFreshdeskApiAsync(request, retriedCount++);
|
||||
}
|
||||
|
||||
private async Task<(HttpResponseMessage, T)> CallOnyxApi<T>(HttpRequestMessage request)
|
||||
{
|
||||
var httpClient = _httpClientFactory.CreateClient("OnyxApi");
|
||||
var response = await httpClient.SendAsync(request);
|
||||
|
||||
if (response.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
{
|
||||
_logger.LogError("Error calling Onyx AI API. Status code: {0}. Response {1}",
|
||||
response.StatusCode, JsonSerializer.Serialize(response));
|
||||
return (null, default);
|
||||
}
|
||||
var responseStr = await response.Content.ReadAsStringAsync();
|
||||
var responseJson = JsonSerializer.Deserialize<T>(responseStr, options: new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
|
||||
return (response, responseJson);
|
||||
}
|
||||
|
||||
private TAttribute GetAttribute<TAttribute>(Enum enumValue) where TAttribute : Attribute
|
||||
{
|
||||
return enumValue.GetType().GetMember(enumValue.ToString()).First().GetCustomAttribute<TAttribute>();
|
||||
|
@ -1,53 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Billing.Controllers;
|
||||
|
||||
public class LoginController : Controller
|
||||
{
|
||||
/*
|
||||
private readonly PasswordlessSignInManager<IdentityUser> _signInManager;
|
||||
|
||||
public LoginController(
|
||||
PasswordlessSignInManager<IdentityUser> signInManager)
|
||||
{
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
public IActionResult Index()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Index(LoginModel model)
|
||||
{
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
var result = await _signInManager.PasswordlessSignInAsync(model.Email,
|
||||
Url.Action("Confirm", "Login", null, Request.Scheme));
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Account not found.");
|
||||
}
|
||||
}
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Confirm(string email, string token)
|
||||
{
|
||||
var result = await _signInManager.PasswordlessSignInAsync(email, token, false);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return View("Error");
|
||||
}
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
*/
|
||||
}
|
@ -32,5 +32,6 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
public static void AddJobsServices(IServiceCollection services)
|
||||
{
|
||||
services.AddTransient<AliveJob>();
|
||||
services.AddTransient<SubscriptionCancellationJob>();
|
||||
}
|
||||
}
|
||||
|
58
src/Billing/Jobs/SubscriptionCancellationJob.cs
Normal file
58
src/Billing/Jobs/SubscriptionCancellationJob.cs
Normal file
@ -0,0 +1,58 @@
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Core.Repositories;
|
||||
using Quartz;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Billing.Jobs;
|
||||
|
||||
public class SubscriptionCancellationJob(
|
||||
IStripeFacade stripeFacade,
|
||||
IOrganizationRepository organizationRepository)
|
||||
: IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var subscriptionId = context.MergedJobDataMap.GetString("subscriptionId");
|
||||
var organizationId = new Guid(context.MergedJobDataMap.GetString("organizationId") ?? string.Empty);
|
||||
|
||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||
if (organization == null || organization.Enabled)
|
||||
{
|
||||
// Organization was deleted or re-enabled by CS, skip cancellation
|
||||
return;
|
||||
}
|
||||
|
||||
var subscription = await stripeFacade.GetSubscription(subscriptionId);
|
||||
if (subscription?.Status != "unpaid")
|
||||
{
|
||||
// Subscription is no longer unpaid, skip cancellation
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel the subscription
|
||||
await stripeFacade.CancelSubscription(subscriptionId, new SubscriptionCancelOptions());
|
||||
|
||||
// Void any open invoices
|
||||
var options = new InvoiceListOptions
|
||||
{
|
||||
Status = "open",
|
||||
Subscription = subscriptionId,
|
||||
Limit = 100
|
||||
};
|
||||
var invoices = await stripeFacade.ListInvoices(options);
|
||||
foreach (var invoice in invoices)
|
||||
{
|
||||
await stripeFacade.VoidInvoice(invoice.Id);
|
||||
}
|
||||
|
||||
while (invoices.HasMore)
|
||||
{
|
||||
options.StartingAfter = invoices.Data.Last().Id;
|
||||
invoices = await stripeFacade.ListInvoices(options);
|
||||
foreach (var invoice in invoices)
|
||||
{
|
||||
await stripeFacade.VoidInvoice(invoice.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
44
src/Billing/Models/FreshdeskViewTicketModel.cs
Normal file
44
src/Billing/Models/FreshdeskViewTicketModel.cs
Normal file
@ -0,0 +1,44 @@
|
||||
namespace Bit.Billing.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
public class FreshdeskViewTicketModel
|
||||
{
|
||||
[JsonPropertyName("spam")]
|
||||
public bool? Spam { get; set; }
|
||||
|
||||
[JsonPropertyName("priority")]
|
||||
public int? Priority { get; set; }
|
||||
|
||||
[JsonPropertyName("source")]
|
||||
public int? Source { get; set; }
|
||||
|
||||
[JsonPropertyName("status")]
|
||||
public int? Status { get; set; }
|
||||
|
||||
[JsonPropertyName("subject")]
|
||||
public string Subject { get; set; }
|
||||
|
||||
[JsonPropertyName("support_email")]
|
||||
public string SupportEmail { get; set; }
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
[JsonPropertyName("description_text")]
|
||||
public string DescriptionText { get; set; }
|
||||
|
||||
[JsonPropertyName("created_at")]
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("updated_at")]
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
[JsonPropertyName("tags")]
|
||||
public List<string> Tags { get; set; }
|
||||
}
|
54
src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs
Normal file
54
src/Billing/Models/OnyxAnswerWithCitationRequestModel.cs
Normal file
@ -0,0 +1,54 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bit.Billing.Models;
|
||||
|
||||
public class OnyxAnswerWithCitationRequestModel
|
||||
{
|
||||
[JsonPropertyName("messages")]
|
||||
public List<Message> Messages { get; set; }
|
||||
|
||||
[JsonPropertyName("persona_id")]
|
||||
public int PersonaId { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName("prompt_id")]
|
||||
public int PromptId { get; set; } = 1;
|
||||
|
||||
[JsonPropertyName("retrieval_options")]
|
||||
public RetrievalOptions RetrievalOptions { get; set; }
|
||||
|
||||
public OnyxAnswerWithCitationRequestModel(string message)
|
||||
{
|
||||
message = message.Replace(Environment.NewLine, " ").Replace('\r', ' ').Replace('\n', ' ');
|
||||
Messages = new List<Message>() { new Message() { MessageText = message } };
|
||||
RetrievalOptions = new RetrievalOptions();
|
||||
}
|
||||
}
|
||||
|
||||
public class Message
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public string MessageText { get; set; }
|
||||
|
||||
[JsonPropertyName("sender")]
|
||||
public string Sender { get; set; } = "user";
|
||||
}
|
||||
|
||||
public class RetrievalOptions
|
||||
{
|
||||
[JsonPropertyName("run_search")]
|
||||
public string RunSearch { get; set; } = RetrievalOptionsRunSearch.Auto;
|
||||
|
||||
[JsonPropertyName("real_time")]
|
||||
public bool RealTime { get; set; } = true;
|
||||
|
||||
[JsonPropertyName("limit")]
|
||||
public int? Limit { get; set; } = 3;
|
||||
}
|
||||
|
||||
public class RetrievalOptionsRunSearch
|
||||
{
|
||||
public const string Always = "always";
|
||||
public const string Never = "never";
|
||||
public const string Auto = "auto";
|
||||
}
|
30
src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs
Normal file
30
src/Billing/Models/OnyxAnswerWithCitationResponseModel.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bit.Billing.Models;
|
||||
|
||||
public class OnyxAnswerWithCitationResponseModel
|
||||
{
|
||||
[JsonPropertyName("answer")]
|
||||
public string Answer { get; set; }
|
||||
|
||||
[JsonPropertyName("rephrase")]
|
||||
public string Rephrase { get; set; }
|
||||
|
||||
[JsonPropertyName("citations")]
|
||||
public List<Citation> Citations { get; set; }
|
||||
|
||||
[JsonPropertyName("llm_selected_doc_indices")]
|
||||
public List<int> LlmSelectedDocIndices { get; set; }
|
||||
|
||||
[JsonPropertyName("error_msg")]
|
||||
public string ErrorMsg { get; set; }
|
||||
}
|
||||
|
||||
public class Citation
|
||||
{
|
||||
[JsonPropertyName("citation_num")]
|
||||
public int CitationNum { get; set; }
|
||||
|
||||
[JsonPropertyName("document_id")]
|
||||
public string DocumentId { get; set; }
|
||||
}
|
@ -80,12 +80,6 @@ public interface IStripeFacade
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<TaxRate> GetTaxRate(
|
||||
string taxRateId,
|
||||
TaxRateGetOptions options = null,
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Discount> DeleteCustomerDiscount(
|
||||
string customerId,
|
||||
RequestOptions requestOptions = null,
|
||||
|
@ -14,19 +14,22 @@ public class CustomerUpdatedHandler : ICustomerUpdatedHandler
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IStripeEventService _stripeEventService;
|
||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||
private readonly ILogger<CustomerUpdatedHandler> _logger;
|
||||
|
||||
public CustomerUpdatedHandler(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IReferenceEventService referenceEventService,
|
||||
ICurrentContext currentContext,
|
||||
IStripeEventService stripeEventService,
|
||||
IStripeEventUtilityService stripeEventUtilityService)
|
||||
IStripeEventUtilityService stripeEventUtilityService,
|
||||
ILogger<CustomerUpdatedHandler> logger)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationRepository = organizationRepository ?? throw new ArgumentNullException(nameof(organizationRepository));
|
||||
_referenceEventService = referenceEventService;
|
||||
_currentContext = currentContext;
|
||||
_stripeEventService = stripeEventService;
|
||||
_stripeEventUtilityService = stripeEventUtilityService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -35,25 +38,76 @@ public class CustomerUpdatedHandler : ICustomerUpdatedHandler
|
||||
/// <param name="parsedEvent"></param>
|
||||
public async Task HandleAsync(Event parsedEvent)
|
||||
{
|
||||
var customer = await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]);
|
||||
if (customer.Subscriptions == null || !customer.Subscriptions.Any())
|
||||
if (parsedEvent == null)
|
||||
{
|
||||
_logger.LogError("Parsed event was null in CustomerUpdatedHandler");
|
||||
throw new ArgumentNullException(nameof(parsedEvent));
|
||||
}
|
||||
|
||||
if (_stripeEventService == null)
|
||||
{
|
||||
_logger.LogError("StripeEventService was not initialized in CustomerUpdatedHandler");
|
||||
throw new InvalidOperationException($"{nameof(_stripeEventService)} is not initialized");
|
||||
}
|
||||
|
||||
var customer = await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]);
|
||||
if (customer?.Subscriptions == null || !customer.Subscriptions.Any())
|
||||
{
|
||||
_logger.LogWarning("Customer or subscriptions were null or empty in CustomerUpdatedHandler. Customer ID: {CustomerId}", customer?.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var subscription = customer.Subscriptions.First();
|
||||
|
||||
if (subscription.Metadata == null)
|
||||
{
|
||||
_logger.LogWarning("Subscription metadata was null in CustomerUpdatedHandler. Subscription ID: {SubscriptionId}", subscription.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_stripeEventUtilityService == null)
|
||||
{
|
||||
_logger.LogError("StripeEventUtilityService was not initialized in CustomerUpdatedHandler");
|
||||
throw new InvalidOperationException($"{nameof(_stripeEventUtilityService)} is not initialized");
|
||||
}
|
||||
|
||||
var (organizationId, _, providerId) = _stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata);
|
||||
|
||||
if (!organizationId.HasValue)
|
||||
{
|
||||
_logger.LogWarning("Organization ID was not found in subscription metadata. Subscription ID: {SubscriptionId}", subscription.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_organizationRepository == null)
|
||||
{
|
||||
_logger.LogError("OrganizationRepository was not initialized in CustomerUpdatedHandler");
|
||||
throw new InvalidOperationException($"{nameof(_organizationRepository)} is not initialized");
|
||||
}
|
||||
|
||||
var organization = await _organizationRepository.GetByIdAsync(organizationId.Value);
|
||||
|
||||
if (organization == null)
|
||||
{
|
||||
_logger.LogWarning("Organization not found. Organization ID: {OrganizationId}", organizationId.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
organization.BillingEmail = customer.Email;
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
if (_referenceEventService == null)
|
||||
{
|
||||
_logger.LogError("ReferenceEventService was not initialized in CustomerUpdatedHandler");
|
||||
throw new InvalidOperationException($"{nameof(_referenceEventService)} is not initialized");
|
||||
}
|
||||
|
||||
if (_currentContext == null)
|
||||
{
|
||||
_logger.LogError("CurrentContext was not initialized in CustomerUpdatedHandler");
|
||||
throw new InvalidOperationException($"{nameof(_currentContext)} is not initialized");
|
||||
}
|
||||
|
||||
await _referenceEventService.RaiseEventAsync(
|
||||
new ReferenceEvent(ReferenceEventType.OrganizationEditedInStripe, organization, _currentContext));
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Enums;
|
||||
|
@ -296,7 +296,7 @@ public class StripeEventUtilityService : IStripeEventUtilityService
|
||||
btObjIdField = "provider_id";
|
||||
btObjId = providerId.Value;
|
||||
}
|
||||
var btInvoiceAmount = invoice.AmountDue / 100M;
|
||||
var btInvoiceAmount = Math.Round(invoice.AmountDue / 100M, 2);
|
||||
|
||||
var existingTransactions = organizationId.HasValue
|
||||
? await _transactionRepository.GetManyByOrganizationIdAsync(organizationId.Value)
|
||||
@ -318,26 +318,34 @@ public class StripeEventUtilityService : IStripeEventUtilityService
|
||||
Result<Braintree.Transaction> transactionResult;
|
||||
try
|
||||
{
|
||||
transactionResult = await _btGateway.Transaction.SaleAsync(
|
||||
new Braintree.TransactionRequest
|
||||
var transactionRequest = new Braintree.TransactionRequest
|
||||
{
|
||||
Amount = btInvoiceAmount,
|
||||
CustomerId = customer.Metadata["btCustomerId"],
|
||||
Options = new Braintree.TransactionOptionsRequest
|
||||
{
|
||||
Amount = btInvoiceAmount,
|
||||
CustomerId = customer.Metadata["btCustomerId"],
|
||||
Options = new Braintree.TransactionOptionsRequest
|
||||
SubmitForSettlement = true,
|
||||
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
||||
{
|
||||
SubmitForSettlement = true,
|
||||
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
||||
{
|
||||
CustomField =
|
||||
$"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
|
||||
}
|
||||
},
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[btObjIdField] = btObjId.ToString(),
|
||||
["region"] = _globalSettings.BaseServiceUri.CloudRegion
|
||||
CustomField =
|
||||
$"{btObjIdField}:{btObjId},region:{_globalSettings.BaseServiceUri.CloudRegion}"
|
||||
}
|
||||
});
|
||||
},
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[btObjIdField] = btObjId.ToString(),
|
||||
["region"] = _globalSettings.BaseServiceUri.CloudRegion
|
||||
}
|
||||
};
|
||||
|
||||
_logger.LogInformation("Creating Braintree transaction with Amount: {Amount}, CustomerId: {CustomerId}, " +
|
||||
"CustomField: {CustomField}, CustomFields: {@CustomFields}",
|
||||
transactionRequest.Amount,
|
||||
transactionRequest.CustomerId,
|
||||
transactionRequest.Options.PayPal.CustomField,
|
||||
transactionRequest.CustomFields);
|
||||
|
||||
transactionResult = await _btGateway.Transaction.SaleAsync(transactionRequest);
|
||||
}
|
||||
catch (NotFoundException e)
|
||||
{
|
||||
@ -345,9 +353,19 @@ public class StripeEventUtilityService : IStripeEventUtilityService
|
||||
"Attempted to make a payment with Braintree, but customer did not exist for the given btCustomerId present on the Stripe metadata");
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, "Exception occurred while trying to pay invoice with Braintree");
|
||||
throw;
|
||||
}
|
||||
|
||||
if (!transactionResult.IsSuccess())
|
||||
{
|
||||
_logger.LogWarning("Braintree transaction failed. Error: {ErrorMessage}, Transaction Status: {Status}, Validation Errors: {ValidationErrors}",
|
||||
transactionResult.Message,
|
||||
transactionResult.Target?.Status,
|
||||
string.Join(", ", transactionResult.Errors.DeepAll().Select(e => $"Code: {e.Code}, Message: {e.Message}, Attribute: {e.Attribute}")));
|
||||
|
||||
if (invoice.AttemptCount < 4)
|
||||
{
|
||||
await _mailService.SendPaymentFailedAsync(customer.Email, btInvoiceAmount, true);
|
||||
|
@ -10,7 +10,6 @@ public class StripeFacade : IStripeFacade
|
||||
private readonly InvoiceService _invoiceService = new();
|
||||
private readonly PaymentMethodService _paymentMethodService = new();
|
||||
private readonly SubscriptionService _subscriptionService = new();
|
||||
private readonly TaxRateService _taxRateService = new();
|
||||
private readonly DiscountService _discountService = new();
|
||||
|
||||
public async Task<Charge> GetCharge(
|
||||
@ -99,13 +98,6 @@ public class StripeFacade : IStripeFacade
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await _subscriptionService.CancelAsync(subscriptionId, options, requestOptions, cancellationToken);
|
||||
|
||||
public async Task<TaxRate> GetTaxRate(
|
||||
string taxRateId,
|
||||
TaxRateGetOptions options = null,
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
await _taxRateService.GetAsync(taxRateId, options, requestOptions, cancellationToken);
|
||||
|
||||
public async Task<Discount> DeleteCustomerDiscount(
|
||||
string customerId,
|
||||
RequestOptions requestOptions = null,
|
||||
|
@ -1,8 +1,12 @@
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Billing.Jobs;
|
||||
using Bit.Core;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Quartz;
|
||||
using Stripe;
|
||||
using Event = Stripe.Event;
|
||||
|
||||
@ -18,6 +22,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly ISchedulerFactory _schedulerFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public SubscriptionUpdatedHandler(
|
||||
IStripeEventService stripeEventService,
|
||||
@ -27,7 +33,9 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand,
|
||||
IUserService userService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IOrganizationRepository organizationRepository)
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISchedulerFactory schedulerFactory,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_stripeEventService = stripeEventService;
|
||||
_stripeEventUtilityService = stripeEventUtilityService;
|
||||
@ -37,6 +45,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
_userService = userService;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_schedulerFactory = schedulerFactory;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -54,6 +64,10 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
when organizationId.HasValue:
|
||||
{
|
||||
await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd);
|
||||
if (subscription.Status == StripeSubscriptionStatus.Unpaid)
|
||||
{
|
||||
await ScheduleCancellationJobAsync(subscription.Id, organizationId.Value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired:
|
||||
@ -182,4 +196,27 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
await _stripeFacade.DeleteSubscriptionDiscount(subscription.Id);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ScheduleCancellationJobAsync(string subscriptionId, Guid organizationId)
|
||||
{
|
||||
var isResellerManagedOrgAlertEnabled = _featureService.IsEnabled(FeatureFlagKeys.ResellerManagedOrgAlert);
|
||||
|
||||
if (isResellerManagedOrgAlertEnabled)
|
||||
{
|
||||
var scheduler = await _schedulerFactory.GetScheduler();
|
||||
|
||||
var job = JobBuilder.Create<SubscriptionCancellationJob>()
|
||||
.WithIdentity($"cancel-sub-{subscriptionId}", "subscription-cancellations")
|
||||
.UsingJobData("subscriptionId", subscriptionId)
|
||||
.UsingJobData("organizationId", organizationId.ToString())
|
||||
.Build();
|
||||
|
||||
var trigger = TriggerBuilder.Create()
|
||||
.WithIdentity($"cancel-trigger-{subscriptionId}", "subscription-cancellations")
|
||||
.StartAt(DateTimeOffset.UtcNow.AddDays(7))
|
||||
.Build();
|
||||
|
||||
await scheduler.ScheduleJob(job, trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -161,18 +160,13 @@ public class UpcomingInvoiceHandler : IUpcomingInvoiceHandler
|
||||
|
||||
private async Task<Subscription> TryEnableAutomaticTaxAsync(Subscription subscription)
|
||||
{
|
||||
var customerGetOptions = new CustomerGetOptions { Expand = ["tax"] };
|
||||
var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions);
|
||||
|
||||
if (subscription.AutomaticTax.Enabled ||
|
||||
customer.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.Supported)
|
||||
if (subscription.AutomaticTax.Enabled)
|
||||
{
|
||||
return subscription;
|
||||
}
|
||||
|
||||
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
DefaultTaxRates = [],
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using System.Net.Http.Headers;
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Billing.Services.Implementations;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
@ -9,6 +10,7 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Quartz;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Billing;
|
||||
@ -33,6 +35,7 @@ public class Startup
|
||||
// Settings
|
||||
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
|
||||
services.Configure<BillingSettings>(Configuration.GetSection("BillingSettings"));
|
||||
var billingSettings = Configuration.GetSection("BillingSettings").Get<BillingSettings>();
|
||||
|
||||
// Stripe Billing
|
||||
StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;
|
||||
@ -96,14 +99,29 @@ public class Startup
|
||||
|
||||
// Set up HttpClients
|
||||
services.AddHttpClient("FreshdeskApi");
|
||||
services.AddHttpClient("OnyxApi", client =>
|
||||
{
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", billingSettings.Onyx.ApiKey);
|
||||
});
|
||||
|
||||
services.AddScoped<IStripeFacade, StripeFacade>();
|
||||
services.AddScoped<IStripeEventService, StripeEventService>();
|
||||
services.AddScoped<IProviderEventService, ProviderEventService>();
|
||||
|
||||
// Add Quartz services first
|
||||
services.AddQuartz(q =>
|
||||
{
|
||||
q.UseMicrosoftDependencyInjectionJobFactory();
|
||||
});
|
||||
services.AddQuartzHostedService();
|
||||
|
||||
// Jobs service
|
||||
Jobs.JobsHostedService.AddJobsServices(services);
|
||||
services.AddHostedService<Jobs.JobsHostedService>();
|
||||
|
||||
// Swagger
|
||||
services.AddEndpointsApiExplorer();
|
||||
services.AddSwaggerGen();
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
@ -120,6 +138,11 @@ public class Startup
|
||||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(c =>
|
||||
{
|
||||
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Billing API V1");
|
||||
});
|
||||
}
|
||||
|
||||
app.UseStaticFiles();
|
||||
|
@ -73,6 +73,10 @@
|
||||
"region": "US",
|
||||
"userFieldName": "cf_user",
|
||||
"orgFieldName": "cf_org"
|
||||
}
|
||||
},
|
||||
"onyx": {
|
||||
"apiKey": "SECRET",
|
||||
"baseUrl": "https://cloud.onyx.app/api"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,6 +103,12 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
/// </summary>
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, members can only delete items when they have a Can Manage permission over the collection.
|
||||
/// If set to false, members can delete items when they have a Can Manage OR Can Edit permission over the collection.
|
||||
/// </summary>
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Risk Insights is a reporting feature that provides insights into the security of an organization's vault.
|
||||
/// </summary>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user