1
0
mirror of https://github.com/bitwarden/web synced 2025-12-11 13:53:17 +00:00

Compare commits

..

9 Commits

Author SHA1 Message Date
Vince Grassia
d10dc94a48 Remove old 'release' ref in workflow (#1328)
(cherry picked from commit 75984a2e37)
2021-12-07 22:52:00 -05:00
github-actions[bot]
c85051d6e2 Bumped version to 2.25.0 (#1327)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(cherry picked from commit 1cba6dc3b9)
2021-12-07 19:13:44 -08:00
Matt Gibson
778700f399 Fix families sponsorship redeem page (#1321)
* Display sponsorship warning when sponsoring an org

Move actions to drop down menu

Fix revoke cancel success popup

* Only show warning when sponsorship exists

(cherry picked from commit d9231ae3f3)
2021-12-01 20:48:44 -05:00
Justin Baur
23048d46d6 Fix basePrice to reflect the sponsorship (#1311)
* Fix basePrice to reflect the sponsorship

* Ran linter

* Add latest copy

* Remove unneeded if

* Fix times

* Stopped hardcoding basePrice

* Stopped hardcoding 40 in UI

* Switch to single small block

* Update jslib

* Revert "Update jslib"

This reverts commit 28534f2230.

* Revert "Remove unneeded if"

This reverts commit 5540b19998.

* Fix revert issue

(cherry picked from commit 4b856d9016)
2021-11-24 15:12:49 -06:00
Matt Gibson
e54586c7d2 Update jslib 2021-11-24 15:48:20 -05:00
Matt Gibson
494fc4b194 Fix formatting and title of sponsoring org drop down (#1317)
(cherry picked from commit 4029554658)
2021-11-24 15:48:00 -05:00
Matt Gibson
48b9393a48 Add sponsorship pre validate to families redeem page (#1315)
* Add sponsorship pre validate to families redeem page

* Update messaging

* update jslib

(cherry picked from commit 6ec22a9408)
2021-11-24 15:47:11 -05:00
Matt Gibson
b6b3184a7b Force sponsorship friendly name to recipient address (#1316)
(cherry picked from commit 9cc7dfb884)
2021-11-24 15:46:45 -05:00
Matt Gibson
66be24a1f5 Display sponsored status for sponsored org subscription (#1312)
* Display sponsored status for sponsored org subscription

* Linter fixes

(cherry picked from commit f8c943c042)
2021-11-24 15:46:23 -05:00
637 changed files with 45660 additions and 73052 deletions

View File

@@ -12,7 +12,7 @@ insert_final_newline = true
[*.{js,ts,scss,html}] [*.{js,ts,scss,html}]
charset = utf-8 charset = utf-8
indent_style = space indent_style = space
indent_size = 2 indent_size = 4
[*.{ts}] [*.{ts}]
quote_type = single quote_type = single

View File

@@ -1,8 +0,0 @@
**/dist
**/build
jslib
webpack.config.js
scripts/optimize.js
config.js
**/node_modules

View File

@@ -1,31 +0,0 @@
{
"root": true,
"env": {
"browser": true
},
"extends": ["./jslib/shared/eslintrc.json"],
"rules": {
"import/order": [
"error",
{
"alphabetize": {
"order": "asc"
},
"newlines-between": "always",
"pathGroups": [
{
"pattern": "jslib-*/**",
"group": "external",
"position": "after"
},
{
"pattern": "src/**/*",
"group": "parent",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin"]
}
]
}
}

View File

@@ -1,2 +0,0 @@
# Apply Prettier https://github.com/bitwarden/web/pull/1347
56477eb39cfd8a73c9920577d24d75fed36e2cf5

4
.gitattributes vendored
View File

@@ -1 +1,3 @@
* text=auto eol=lf *.sh eol=lf
.dockerignore eol=lf
dockerfile eol=lf

View File

@@ -1,5 +1,4 @@
## Type of change ## Type of change
- [ ] Bug fix - [ ] Bug fix
- [ ] New feature development - [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
@@ -7,22 +6,27 @@
- [ ] Other - [ ] Other
## Objective ## Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding--> <!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
## Code changes
## Code changes
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes--> <!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories--> <!--Also refer to any related changes or PRs in other repositories-->
- **file.ext:** Description of what was changed and why * **file.ext:** Description of what was changed and why
## Screenshots ## Screenshots
<!--Required for any UI changes. Delete if not applicable--> <!--Required for any UI changes. Delete if not applicable-->
## Before you submit
## Testing requirements
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required) - [ ] I have checked for **linting** errors (`npm run lint`) (required)
- [ ] This change requires a **documentation update** (notify the documentation team) - [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team) - [ ] This change has particular **deployment requirements** (notify the DevOps team)

View File

@@ -9,12 +9,8 @@ on:
required: false required: false
push: push:
branches-ignore: branches-ignore:
- "l10n_master" - 'l10n_master'
- "gh-pages" - 'gh-pages'
- "deploy"
paths-ignore:
- '.github/workflows/**'
jobs: jobs:
cloc: cloc:
@@ -33,27 +29,6 @@ jobs:
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
lint:
name: Lint
runs-on: ubuntu-20.04
steps:
- name: Checkout repo
uses: actions/checkout@a12a3943b4bdde767164f792f33f40b04645d846
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-lint-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -65,27 +40,32 @@ jobs:
- name: Get GitHub sha as version - name: Get GitHub sha as version
id: version id: version
run: echo "::set-output name=value::${GITHUB_SHA:0:7}" run: |
echo "::set-output name=value::${GITHUB_SHA:0:7}"
build-oss-selfhost: build-oss-selfhost:
name: Build OSS zip name: Build OSS zip
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: needs: setup
- setup
- lint
env: env:
_VERSION: ${{ needs.setup.outputs.version }} _VERSION: ${{ needs.setup.outputs.version }}
steps: steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node - name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0 uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
cache: 'npm' node-version: '14'
cache-dependency-path: '**/package-lock.json'
node-version: "16" - name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
run: | run: |
@@ -97,6 +77,9 @@ jobs:
echo "GitHub ref: $GITHUB_REF" echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT" echo "GitHub event: $GITHUB_EVENT"
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -116,21 +99,25 @@ jobs:
build-cloud: build-cloud:
name: Build Cloud zip name: Build Cloud zip
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: needs: setup
- setup
- lint
env: env:
_VERSION: ${{ needs.setup.outputs.version }} _VERSION: ${{ needs.setup.outputs.version }}
steps: steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node - name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0 uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
cache: 'npm' node-version: '14'
cache-dependency-path: '**/package-lock.json'
node-version: "16" - name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
run: | run: |
@@ -142,6 +129,9 @@ jobs:
echo "GitHub ref: $GITHUB_REF" echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT" echo "GitHub event: $GITHUB_EVENT"
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -161,21 +151,25 @@ jobs:
build-commercial-selfhost: build-commercial-selfhost:
name: Build SelfHost Docker image name: Build SelfHost Docker image
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: needs: setup
- setup
- lint
env: env:
_VERSION: ${{ needs.setup.outputs.version }} _VERSION: ${{ needs.setup.outputs.version }}
steps: steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node - name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0 uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
cache: 'npm' node-version: '14'
cache-dependency-path: '**/package-lock.json'
node-version: "16" - name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
run: | run: |
@@ -188,13 +182,19 @@ jobs:
echo "GitHub event: $GITHUB_EVENT" echo "GitHub event: $GITHUB_EVENT"
- name: Setup DCT - name: Setup DCT
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/release'
id: setup-dct id: setup-dct
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
with: with:
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
azure-keyvault-name: "bitwarden-prod-kv" azure-keyvault-name: "bitwarden-prod-kv"
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Restore
run: dotnet tool restore
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -228,12 +228,12 @@ jobs:
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
run: docker tag bitwarden/web bitwarden/web:dev run: docker tag bitwarden/web bitwarden/web:dev
- name: Tag hotfix branch - name: Tag release branch
if: github.ref == 'refs/heads/hotfix-rc' if: github.ref == 'refs/heads/release'
run: docker tag bitwarden/web bitwarden/web:hotfix-rc run: docker tag bitwarden/web bitwarden/web:latest
- name: List Docker images - name: List Docker images
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/release'
run: docker images run: docker images
- name: Push rc image - name: Push rc image
@@ -250,59 +250,37 @@ jobs:
DOCKER_CONTENT_TRUST: 1 DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }} DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
- name: Push hotfix image - name: Push latest image
if: github.ref == 'refs/heads/hotfix-rc' if: github.ref == 'refs/heads/release'
run: docker push bitwarden/web:hotfix-rc run: docker push bitwarden/web:latest
env: env:
DOCKER_CONTENT_TRUST: 1 DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }} DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
- name: Log out of Docker - name: Log out of Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc' if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/release'
run: |
docker logout
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
- name: Login to Azure - QA Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
- name: Login to Azure ACR
run: az acr login -n bitwardenqa
- name: Tag and Push RC to Azure ACR QA registry
env:
REGISTRY: bitwardenqa.azurecr.io
run: |
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") # slash safe branch name
if [[ "$IMAGE_TAG" == "master" ]]; then
IMAGE_TAG=dev
fi
docker tag bitwarden/web \
$REGISTRY/web-sh:$IMAGE_TAG
docker push $REGISTRY/web-sh:$IMAGE_TAG
- name: Log out of Docker
run: docker logout run: docker logout
build-qa: build-qa:
name: Build Docker images for QA environment name: Build Docker images for QA environment
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs:
- setup
- lint
steps: steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up Node - name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0 uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
cache: 'npm' node-version: '14'
cache-dependency-path: '**/package-lock.json'
node-version: "16" - name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
run: | run: |
@@ -322,6 +300,12 @@ jobs:
- name: Log into container registry - name: Log into container registry
run: az acr login -n bitwardenqa run: az acr login -n bitwardenqa
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Restore
run: dotnet tool restore
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
@@ -345,7 +329,7 @@ jobs:
- name: Get image tag - name: Get image tag
id: image-tag id: image-tag
run: | run: |
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }} TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }}
if [[ $TAG_EXTENSION ]]; then if [[ $TAG_EXTENSION ]]; then
@@ -382,24 +366,35 @@ jobs:
name: Test code on Windows name: Test code on Windows
runs-on: windows-2019 runs-on: windows-2019
steps: steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Set up NuGet - name: Set up NuGet
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1 uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
with: with:
nuget-version: "latest" nuget-version: 'latest'
- name: Set up MSBuild
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@9ced9a43a244f3ac94f13bfd896db8c8f30da67a # v3.0.0 uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
cache: 'npm' node-version: '14'
cache-dependency-path: '**/package-lock.json'
node-version: "16" - name: Update NPM
run: |
npm install -g npm@7
- name: Print environment - name: Print environment
run: | run: |
nuget help | grep Version nuget help | grep Version
msbuild -version
dotnet --info
node --version node --version
npm --version npm --version
echo "GitHub ref: $GITHUB_REF" echo "GitHub ref: $GITHUB_REF"
@@ -408,9 +403,15 @@ jobs:
GITHUB_REF: ${{ github.ref }} GITHUB_REF: ${{ github.ref }}
GITHUB_EVENT: ${{ github.event_name }} GITHUB_EVENT: ${{ github.event_name }}
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: NPM install
run: npm ci
- name: NPM build - name: NPM build
run: npm run build:bit:cloud run: npm run build:bit:cloud
@@ -461,7 +462,6 @@ jobs:
needs: needs:
- cloc - cloc
- setup - setup
- lint
- build-oss-selfhost - build-oss-selfhost
- build-cloud - build-cloud
- build-commercial-selfhost - build-commercial-selfhost
@@ -473,7 +473,6 @@ jobs:
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }} if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
env: env:
CLOC_STATUS: ${{ needs.cloc.result }} CLOC_STATUS: ${{ needs.cloc.result }}
LINT_STATUS: ${{ needs.lint.result }}
SETUP_STATUS: ${{ needs.setup.result }} SETUP_STATUS: ${{ needs.setup.result }}
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }} BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }} BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
@@ -484,8 +483,6 @@ jobs:
run: | run: |
if [ "$CLOC_STATUS" = "failure" ]; then if [ "$CLOC_STATUS" = "failure" ]; then
exit 1 exit 1
elif [ "$LINT_STATUS" = "failure" ]; then
exit 1
elif [ "$SETUP_STATUS" = "failure" ]; then elif [ "$SETUP_STATUS" = "failure" ]; then
exit 1 exit 1
elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then

View File

@@ -5,7 +5,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: {} inputs: {}
schedule: schedule:
- cron: "0 0 * * 5" - cron: '0 0 * * 5'
jobs: jobs:
crowdin-pull: crowdin-pull:

View File

@@ -1,16 +0,0 @@
---
name: Enforce PR labels
on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
jobs:
enforce-label:
name: EnforceLabel
runs-on: ubuntu-20.04
steps:
- name: Enforce Label
uses: yogevbd/enforce-label-action@8d1e1709b1011e6d90400a0e6cf7c0b77aa5efeb
with:
BANNED_LABELS: "hold"
BANNED_LABELS_DESCRIPTION: "PRs on hold cannot be merged"

View File

@@ -9,8 +9,8 @@ on:
required: false required: false
env: env:
_QA_CLUSTER_RESOURCE_GROUP: "bw-env-qa" _QA_CLUSTER_RESOURCE_GROUP: "bitwarden-devops"
_QA_CLUSTER_NAME: "bw-aks-qa" _QA_CLUSTER_NAME: "dev-aks"
_QA_K8S_NAMESPACE: "bw-qa" _QA_K8S_NAMESPACE: "bw-qa"
_QA_K8S_APP_NAME: "bw-web" _QA_K8S_APP_NAME: "bw-web"
@@ -23,7 +23,8 @@ jobs:
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup - name: Setup
run: export PATH=$PATH:~/work/web/web run:
export PATH=$PATH:~/work/web/web
- name: Login to Azure - name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
@@ -35,16 +36,16 @@ jobs:
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403 uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with: with:
keyvault: "bitwarden-qa-kv" keyvault: "bitwarden-qa-kv"
secrets: "qa-aks-kubectl-credentials" secrets: "dev-aks-kubectl-credentials"
- name: Login with qa-aks-kubectl-credentials SP - name: Login to dev-aks-kubectl SP
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with: with:
creds: ${{ env.qa-aks-kubectl-credentials }} creds: ${{ env.dev-aks-kubectl-credentials }}
- name: Setup AKS access - name: Setup AKS access
#env: env:
# USER_ID: ${{ env.qa-kubectl-managed-identity-clientId }} USER_ID: ${{ env.qa-kubectl-managed-identity-clientId }}
run: | run: |
echo "---az install---" echo "---az install---"
az aks install-cli --install-location ./kubectl --kubelogin-install-location ./kubelogin az aks install-cli --install-location ./kubectl --kubelogin-install-location ./kubelogin
@@ -54,7 +55,7 @@ jobs:
- name: Get image tag - name: Get image tag
id: image_tag id: image_tag
run: | run: |
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
TAG_EXTENSION=${{ github.event.inputs.image_extension }} TAG_EXTENSION=${{ github.event.inputs.image_extension }}
if [[ $TAG_EXTENSION ]]; then if [[ $TAG_EXTENSION ]]; then

View File

@@ -3,16 +3,7 @@ name: Release
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs: {}
release_type:
description: 'Release Options'
required: true
default: 'Initial Release'
type: choice
options:
- Initial Release
- Redeploy
- Dry Run
jobs: jobs:
setup: setup:
@@ -21,20 +12,19 @@ jobs:
outputs: outputs:
release_version: ${{ steps.version.outputs.package }} release_version: ${{ steps.version.outputs.package }}
tag_version: ${{ steps.version.outputs.tag }} tag_version: ${{ steps.version.outputs.tag }}
branch_name: ${{ steps.branch.outputs.branch_name }} branch-name: ${{ steps.branch.outputs.branch-name }}
steps: steps:
- name: Branch check - name: Branch check
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: | run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then
echo "===================================" echo "==================================="
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches" echo "[!] Can only release from the 'rc' or 'hotfix' branches"
echo "===================================" echo "==================================="
exit 1 exit 1
fi fi
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # 2.4.0 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4
- name: Check Release Version - name: Check Release Version
id: version id: version
@@ -44,8 +34,7 @@ jobs:
curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name" curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name"
) )
if [ "v$version" == "$previous_release_tag_version" ] && \ if [ "v$version" == "$previous_release_tag_version" ]; then
[ "${{ github.event.inputs.release_type }}" == "Initial Release" ]; then
echo "[!] Already released v$version. Please bump version to continue" echo "[!] Already released v$version. Please bump version to continue"
exit 1 exit 1
fi fi
@@ -57,7 +46,7 @@ jobs:
id: branch id: branch
run: | run: |
BRANCH_NAME=$(basename ${{ github.ref }}) BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch_name::$BRANCH_NAME" echo "::set-output name=branch-name::$BRANCH_NAME"
self-host: self-host:
@@ -65,9 +54,7 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: setup needs: setup
env: env:
_BRANCH_NAME: ${{ needs.setup.outputs.branch_name }}
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }} _RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_RELEASE_OPTION: ${{ github.event.inputs.release_type }}
steps: steps:
- name: Print environment - name: Print environment
run: | run: |
@@ -75,12 +62,7 @@ jobs:
docker --version docker --version
echo "GitHub ref: $GITHUB_REF" echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT" echo "GitHub event: $GITHUB_EVENT"
echo "Github Release Option: $_RELEASE_OPTION"
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
########## DockerHub ##########
- name: Setup DCT - name: Setup DCT
id: setup-dct id: setup-dct
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
@@ -88,77 +70,32 @@ jobs:
azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} azure-creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
azure-keyvault-name: "bitwarden-prod-kv" azure-keyvault-name: "bitwarden-prod-kv"
- name: Pull latest selfhost image - name: Checkout repo
run: | uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
docker pull bitwarden/web:latest
else
docker pull bitwarden/web:$_BRANCH_NAME
fi
- name: Tag version and latest - name: Pull latest selfhost Release image
run: | run: docker pull bitwarden/web:latest
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
docker tag bitwarden/web:latest bitwarden/web:dryrun
else
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest
fi
- name: Push version and latest image - name: Tag version
if: ${{ github.event.inputs.release_type != 'Dry Run' }} run: |
docker tag bitwarden/web:latest bitwarden/web:$_RELEASE_VERSION
- name: List Docker images
run: docker images
- name: Push images
run: |
docker push bitwarden/web:$_RELEASE_VERSION
env: env:
DOCKER_CONTENT_TRUST: 1 DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }} DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
run: |
docker push bitwarden/web:$_RELEASE_VERSION
docker push bitwarden/web:latest
- name: Log out of Docker and disable Docker Notary
run: |
docker logout
echo "DOCKER_CONTENT_TRUST=0" >> $GITHUB_ENV
########## ACR ##########
- name: Login to Azure - QA Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_QA_KV_CREDENTIALS }}
- name: Login to Azure ACR
run: az acr login -n bitwardenqa
- name: Tag version and latest
env:
REGISTRY: bitwardenqa.azurecr.io
run: |
if [[ "${{ github.event.inputs.release_type }}" == "Dry Run" ]]; then
docker tag bitwarden/web:latest $REGISTRY/web:dryrun
else
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web:latest
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web-sh:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME $REGISTRY/web-sh:latest
fi
- name: Push version and latest image
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
env:
REGISTRY: bitwardenqa.azurecr.io
run: |
docker push $REGISTRY/web:$_RELEASE_VERSION
docker push $REGISTRY/web:latest
docker push $REGISTRY/web-sh:$_RELEASE_VERSION
docker push $REGISTRY/web-sh:latest
- name: Log out of Docker - name: Log out of Docker
run: docker logout run: docker logout
ghpages-deploy: ghpages-deploy:
name: Deploy Web Vault to GitHub Pages name: Deploy Web Vault
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
needs: needs:
- setup - setup
@@ -168,17 +105,17 @@ jobs:
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }} _TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
steps: steps:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
with: with:
ref: gh-pages ref: gh-pages
- name: Create gh-pages-deploy branch - name: Create deploy branch
run: | run: |
git switch -c gh-pages-deploy-$_TAG_VERSION git switch -c deploy-$_TAG_VERSION
git push -u origin gh-pages-deploy-$_TAG_VERSION git push -u origin deploy-$_TAG_VERSION
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup git config - name: Setup git config
run: | run: |
@@ -188,11 +125,11 @@ jobs:
git config --global url."https://".insteadOf ssh:// git config --global url."https://".insteadOf ssh://
- name: Download latest cloud asset - name: Download latest cloud asset
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8 uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch_name }} branch: ${{ needs.setup.outputs.branch-name }}
artifacts: web-*-cloud-COMMERCIAL.zip artifacts: web-*-cloud-COMMERCIAL.zip
# This should result in a build directory in the current working directory # This should result in a build directory in the current working directory
@@ -200,92 +137,26 @@ jobs:
run: unzip web-*-cloud-COMMERCIAL.zip run: unzip web-*-cloud-COMMERCIAL.zip
- name: Deploy GitHub Pages - name: Deploy GitHub Pages
uses: crazy-max/ghaction-github-pages@a117e4aa1fb4854d021546d2abdfac95be568a3a # v2.6.0 uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
target_branch: gh-pages-deploy-${{ needs.setup.outputs.tag_version }} target_branch: deploy-${{ needs.setup.outputs.tag_version }}
build_dir: build build_dir: build
keep_history: true keep_history: true
commit_message: "Staging deploy ${{ needs.setup.outputs.release_version }}" commit_message: "Staging deploy ${{ needs.setup.outputs.release_version }}"
dry_run: ${{ github.event.inputs.release_type == 'Dry Run' }}
- name: Create GitHub Pages Deploy PR - name: Create Deploy PR
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
env: env:
PR_BRANCH: gh-pages-deploy-${{ env._TAG_VERSION }} PR_BRANCH: deploy-${{ env._TAG_VERSION }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
gh pr create --title "Deploy $_RELEASE_VERSION to GitHub Pages" \ gh pr create --title "Deploy $_RELEASE_VERSION" \
--body "Deploying $_RELEASE_VERSION" \ --body "Deploying $_RELEASE_VERSION" \
--base gh-pages \ --base gh-pages \
--head "$PR_BRANCH" --head "$PR_BRANCH"
cfpages-deploy:
name: Deploy Web Vault to CloudFlare Pages branch
runs-on: ubuntu-20.04
needs:
- setup
- self-host
env:
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
steps:
- name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
- name: Download latest cloud asset
uses: bitwarden/gh-actions/download-artifacts@c1fa8e09871a860862d6bbe36184b06d2c7e35a8
with:
workflow: build.yml
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch_name }}
artifacts: web-*-cloud-COMMERCIAL.zip
# This should result in a build directory in the current working directory
- name: Unzip build asset
run: unzip web-*-cloud-COMMERCIAL.zip
- name: Checkout Repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0
with:
ref: deploy
path: deployment
- name: Setup git config
run: |
git config --global user.name = "GitHub Action Bot"
git config --global user.email = "<>"
git config --global url."https://github.com/".insteadOf ssh://git@github.com/
git config --global url."https://".insteadOf ssh://
- name: Deploy CloudFlare Pages
run: |
rm -rf ./*
cp -R ../build/* .
working-directory: deployment
- name: Create cf-pages-deploy branch
run: |
git switch -c cf-pages-deploy-$_TAG_VERSION
git add .
git commit -m "Staging deploy ${{ needs.setup.outputs.release_version }}"
git push -u origin cf-pages-deploy-$_TAG_VERSION
working-directory: deployment
- name: Create CloudFlare Pages Deploy PR
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
env:
PR_BRANCH: cf-pages-deploy-${{ env._TAG_VERSION }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr create --title "Deploy $_RELEASE_VERSION to CloudFlare Pages" \
--body "Deploying $_RELEASE_VERSION" \
--base deploy \
--head "$PR_BRANCH"
release: release:
name: Create GitHub Release name: Create GitHub Release
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -293,14 +164,13 @@ jobs:
- setup - setup
- self-host - self-host
- ghpages-deploy - ghpages-deploy
- cfpages-deploy
steps: steps:
- name: Download latest build artifacts - name: Download latest build artifacts
uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783 uses: bitwarden/gh-actions/download-artifacts@23433be15ed6fd046ce12b6889c5184a8d9c8783
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch_name }} branch: ${{ needs.setup.outputs.branch-name }}
artifacts: "web-*-selfhosted-COMMERCIAL.zip, artifacts: "web-*-selfhosted-COMMERCIAL.zip,
web-*-selfhosted-open-source.zip" web-*-selfhosted-open-source.zip"
@@ -310,8 +180,7 @@ jobs:
mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip mv web-*-selfhosted-open-source.zip web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip
- name: Create release - name: Create release
if: ${{ github.event.inputs.release_type != 'Dry Run' }} uses: ncipollo/release-action@95215a3cb6e6a1908b3c44e00b4fdb15548b1e09
uses: ncipollo/release-action@40bb172bd05f266cf9ba4ff965cb61e9ee5f6d01
with: with:
name: "Version ${{ needs.setup.outputs.release_version }}" name: "Version ${{ needs.setup.outputs.release_version }}"
commit: ${{ github.sha }} commit: ${{ github.sha }}
@@ -321,23 +190,3 @@ jobs:
web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip" web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
draft: true draft: true
dry-run:
name: Dry Run Cleanup
runs-on: ubuntu-20.04
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
env:
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
needs:
- setup
- release
steps:
- name: Checkout repo
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # 2.4.0
- name: Remove gh-pages-deploy branch
run: git push origin --delete gh-pages-deploy-$_TAG_VERSION
- name: Remove cf-pages-deploy branch
run: git push origin --delete cf-pages-deploy-$_TAG_VERSION

View File

@@ -1,71 +0,0 @@
---
name: Version Bump
on:
workflow_dispatch:
inputs:
version_number:
description: "New Version"
required: true
jobs:
bump_props_version:
name: "Create version_bump_${{ github.event.inputs.version_number }} branch"
runs-on: ubuntu-20.04
steps:
- name: Checkout Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
- name: Create Version Branch
run: |
git switch -c version_bump_${{ github.event.inputs.version_number }}
git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Checkout Version Branch
uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579
with:
ref: version_bump_${{ github.event.inputs.version_number }}
- name: Bump Version - package.json
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./package.json"
- name: Bump Version - package-lock.json
uses: bitwarden/gh-actions/version-bump@03ad9a873c39cdc95dd8d77dbbda67f84db43945
with:
version: ${{ github.event.inputs.version_number }}
file_path: "./package-lock.json"
- name: Commit files
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git commit -m "Bumped version to ${{ github.event.inputs.version_number }}" -a
- name: Push changes
run: git push -u origin version_bump_${{ github.event.inputs.version_number }}
- name: Create Version PR
env:
PR_BRANCH: "version_bump_${{ github.event.inputs.version_number }}"
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
BASE_BRANCH: master
TITLE: "Bump version to ${{ github.event.inputs.version_number }}"
run: |
gh pr create --title "$TITLE" \
--base "$BASE" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
--body "
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
Automated version bump to ${{ github.event.inputs.version_number }}"

View File

@@ -1,11 +0,0 @@
---
name: Workflow Linter
on:
pull_request:
paths:
- .github/workflows/**
jobs:
call-workflow:
uses: bitwarden/gh-actions/.github/workflows/workflow-linter.yml@master

View File

@@ -1,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

View File

@@ -1,12 +0,0 @@
# Build directories
build
dist
jslib
# External libraries / auto synced locales
src/locales
src/404/*.min.css
# Github Workflows
.github/workflows

View File

@@ -1,3 +0,0 @@
{
"printWidth": 100
}

View File

@@ -6,12 +6,17 @@ Please visit our [Community Forums](https://community.bitwarden.com/) for genera
Here is how you can get involved: Here is how you can get involved:
- **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one * **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
- **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
- **Report a bug or submit a bugfix:** Use Github issues and pull requests * **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
- **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
- **Help other users:** Go to the [Ask the Bitwarden Community category](https://community.bitwarden.com/c/support/) on the Community Forums * **Report a bug or submit a bugfix:** Use Github issues and pull requests
- **Translate:** See the localization (l10n) section below
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
* **Translate:** See the localization (l10n) section below
## Contributor Agreement ## Contributor Agreement
@@ -19,9 +24,9 @@ Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web)
## Pull Request Guidelines ## Pull Request Guidelines
- use `npm run lint` and fix any linting suggestions before submitting a pull request * use `npm run lint` and fix any linting suggestions before submitting a pull request
- commit any pull requests against the `master` branch * commit any pull requests against the `master` branch
- include a link to your Community Forums post * include a link to your Community Forums post
# Localization (l10n) # Localization (l10n)
@@ -31,6 +36,6 @@ We use a translation tool called [Crowdin](https://crowdin.com) to help manage o
If you are interested in helping translate the Bitwarden web vault into another language (or make a translation correction), please register an account at Crowdin and join our project here: https://crowdin.com/project/bitwarden-web If you are interested in helping translate the Bitwarden web vault into another language (or make a translation correction), please register an account at Crowdin and join our project here: https://crowdin.com/project/bitwarden-web
If the language that you are interested in translating is not already listed, create a new account on Crowdin, join the project, and contact the project owner (https://crowdin.com/profile/dwbit). If the language that you are interested in translating is not already listed, create a new account on Crowdin, join the project, and contact the project owner (https://crowdin.com/profile/kspearrin).
You can read Crowdin's getting started guide for translators here: https://support.crowdin.com/crowdin-intro/ You can read Crowdin's getting started guide for translators here: https://support.crowdin.com/crowdin-intro/

View File

@@ -1,9 +1,3 @@
> **Repository Reorganization in Progress**
>
> We are currently migrating some projects over to a mono repository. For existing PR's we will be providing documentation on how to move/migrate them. To minimize the overhead we are actively reviewing open PRs. If possible please ensure any pending comments are resolved as soon as possible.
>
> New pull requests created during this transition period may not get addressed —if needed, please create a new PR after the reorganization is complete.
<p align="center"> <p align="center">
<img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/web-vault-macbook.png" alt="" width="600" height="358" /> <img src="https://raw.githubusercontent.com/bitwarden/brand/master/screenshots/web-vault-macbook.png" alt="" width="600" height="358" />
</p> </p>
@@ -29,8 +23,8 @@
### Requirements ### Requirements
- [Node.js](https://nodejs.org) v16.13.1 or greater - [Node.js](https://nodejs.org) v14.17 or greater
- NPM v8 - NPM v7
### Run the app ### Run the app
@@ -59,40 +53,18 @@ You can also manually adjusting your API endpoint settings by adding `config/loc
"proxyIdentity": "http://your-identity-url", "proxyIdentity": "http://your-identity-url",
"proxyEvents": "http://your-events-url", "proxyEvents": "http://your-events-url",
"proxyNotifications": "http://your-notifications-url", "proxyNotifications": "http://your-notifications-url",
"allowedHosts": ["hostnames-to-allow-in-webpack"] "allowedHosts": ["hostnames-to-allow-in-webpack"],
}, },
"urls": {} "urls": {
}
} }
``` ```
Where the `urls` object is defined by the [Urls type in jslib](https://github.com/bitwarden/jslib/blob/master/common/src/abstractions/environment.service.ts). Where the `urls` object is defined by the [Urls type in jslib](https://github.com/bitwarden/jslib/blob/master/common/src/abstractions/environment.service.ts).
## We're Hiring!
Interested in contributing in a big way? Consider joining our team! We're hiring for many positions. Please take a look at our [Careers page](https://bitwarden.com/careers/) to see what opportunities are currently open as well as what it's like to work at Bitwarden.
## Contribute ## Contribute
Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file. Code contributions are welcome! Please commit any pull requests against the `master` branch. Learn more about how to contribute by reading the [`CONTRIBUTING.md`](CONTRIBUTING.md) file.
Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file. Security audits and feedback are welcome. Please open an issue or email us privately if the report is sensitive in nature. You can read our security policy in the [`SECURITY.md`](SECURITY.md) file.
## Prettier
We recently migrated to using Prettier as code formatter. All previous branches will need to updated to avoid large merge conflicts using the following steps:
1. Check out your local Branch
2. Run `git merge 2b0a9d995e0147601ca8ae4778434a19354a60c2`
3. Resolve any merge conflicts, commit.
4. Run `npm run prettier`
5. Commit
6. Run `git merge -Xours 56477eb39cfd8a73c9920577d24d75fed36e2cf5`
7. Push
### Git blame
We also recommend that you configure git to ignore the prettier revision using:
```bash
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

View File

@@ -1,11 +1,39 @@
Bitwarden believes that working with security researchers across the globe is crucial to keeping our users safe. If you believe you've found a security issue in our product or service, we encourage you to please submit a report through our [HackerOne Program](https://hackerone.com/bitwarden/). We welcome working with you to resolve the issue promptly. Thanks in advance! Bitwarden believes that working with security researchers across the globe is crucial to keeping our
users safe. If you believe you've found a security issue in our product or service, we encourage you to
notify us. We welcome working with you to resolve the issue promptly. Thanks in advance!
# Disclosure Policy # Disclosure Policy
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every effort to quickly resolve the issue. - Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a third-party. We may publicly disclose the issue before resolving it, if appropriate. effort to quickly resolve the issue.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service. Only interact with accounts you own or with explicit permission of the account holder. - Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
- If you would like to encrypt your report, please use the PGP key with long ID `0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool). third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder.
- If you would like to encrypt your report, please use the PGP key with long ID
`0xDE6887086F892325FEC04CC0D847525B6931381F` (available in the public keyserver pool).
# In-scope
- Security issues in any current release of Bitwarden. This includes the web vault, browser extension,
and mobile apps (iOS and Android). Product downloads are available at https://bitwarden.com. Source
code is available at https://github.com/bitwarden.
# Exclusions
The following bug classes are out-of scope:
- Bugs that are already reported on any of Bitwarden's issue trackers (https://github.com/bitwarden),
or that we already know of. Note that some of our issue tracking is private.
- Issues in an upstream software dependency (ex: Xamarin, ASP.NET) which are already reported to the
upstream maintainer.
- Attacks requiring physical access to a user's device.
- Self-XSS
- Issues related to software or protocols not under Bitwarden's control
- Vulnerabilities in outdated versions of Bitwarden
- Missing security best practices that do not directly lead to a vulnerability
- Issues that do not have any impact on the general public
While researching, we'd like to ask you to refrain from: While researching, we'd like to ask you to refrain from:
@@ -14,8 +42,4 @@ While researching, we'd like to ask you to refrain from:
- Social engineering (including phishing) of Bitwarden staff or contractors - Social engineering (including phishing) of Bitwarden staff or contractors
- Any physical attempts against Bitwarden property or data centers - Any physical attempts against Bitwarden property or data centers
# We want to help you!
If you have something that you feel is close to exploitation, or if you'd like some information regarding the internal API, or generally have any questions regarding the app that would help in your efforts, please email us at https://bitwarden.com/contact and ask for that information. As stated above, Bitwarden wants to help you find issues, and is more than willing to help.
Thank you for helping keep Bitwarden and our users safe! Thank you for helping keep Bitwarden and our users safe!

View File

@@ -1,10 +1,10 @@
import { NgModule } from "@angular/core"; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [ const routes: Routes = [
{ {
path: "providers", path: 'providers',
loadChildren: async () => (await import("./providers/providers.module")).ProvidersModule, loadChildren: async () => (await import('./providers/providers.module')).ProvidersModule,
}, },
]; ];
@@ -12,4 +12,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],
}) })
export class AppRoutingModule {} export class AppRoutingModule { }

View File

@@ -1,15 +1,15 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { AppComponent as BaseAppComponent } from "src/app/app.component"; import { AppComponent as BaseAppComponent } from 'src/app/app.component';
import { DisablePersonalVaultExportPolicy } from './policies/disable-personal-vault-export.component';
import { DisablePersonalVaultExportPolicy } from "./policies/disable-personal-vault-export.component"; import { MaximumVaultTimeoutPolicy } from './policies/maximum-vault-timeout.component';
import { MaximumVaultTimeoutPolicy } from "./policies/maximum-vault-timeout.component";
@Component({ @Component({
selector: "app-root", selector: 'app-root',
templateUrl: "../../../src/app/app.component.html", templateUrl: '../../../src/app/app.component.html',
}) })
export class AppComponent extends BaseAppComponent { export class AppComponent extends BaseAppComponent {
ngOnInit() { ngOnInit() {
super.ngOnInit(); super.ngOnInit();
@@ -18,4 +18,5 @@ export class AppComponent extends BaseAppComponent {
new DisablePersonalVaultExportPolicy(), new DisablePersonalVaultExportPolicy(),
]); ]);
} }
} }

View File

@@ -1,46 +1,44 @@
import { DragDropModule } from "@angular/cdk/drag-drop"; import { ToasterModule } from 'angular2-toaster';
import { OverlayModule } from "@angular/cdk/overlay"; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { RouterModule } from "@angular/router";
import { InfiniteScrollModule } from "ngx-infinite-scroll";
import { JslibModule } from "jslib-angular/jslib.module"; import { DragDropModule } from '@angular/cdk/drag-drop';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { OssRoutingModule } from "src/app/oss-routing.module"; import { AppRoutingModule } from './app-routing.module';
import { OssModule } from "src/app/oss.module"; import { AppComponent } from './app.component';
import { ServicesModule } from "src/app/services/services.module"; import { OrganizationsModule } from './organizations/organizations.module';
import { WildcardRoutingModule } from "src/app/wildcard-routing.module"; import { DisablePersonalVaultExportPolicyComponent } from './policies/disable-personal-vault-export.component';
import { MaximumVaultTimeoutPolicyComponent } from './policies/maximum-vault-timeout.component';
import { AppRoutingModule } from "./app-routing.module"; import { OssRoutingModule } from 'src/app/oss-routing.module';
import { AppComponent } from "./app.component"; import { OssModule } from 'src/app/oss.module';
import { OrganizationsModule } from "./organizations/organizations.module"; import { ServicesModule } from 'src/app/services/services.module';
import { DisablePersonalVaultExportPolicyComponent } from "./policies/disable-personal-vault-export.component"; import { WildcardRoutingModule } from 'src/app/wildcard-routing.module';
import { MaximumVaultTimeoutPolicyComponent } from "./policies/maximum-vault-timeout.component";
@NgModule({ @NgModule({
imports: [ imports: [
OverlayModule,
OssModule, OssModule,
JslibModule,
BrowserAnimationsModule, BrowserAnimationsModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
ServicesModule, ServicesModule,
ToasterModule.forRoot(),
InfiniteScrollModule, InfiniteScrollModule,
DragDropModule, DragDropModule,
AppRoutingModule, AppRoutingModule,
OssRoutingModule, OssRoutingModule,
OrganizationsModule, // Must be after OssRoutingModule for competing routes to resolve properly OrganizationsModule,
RouterModule, RouterModule,
WildcardRoutingModule, // Needs to be last to catch all non-existing routes WildcardRoutingModule, // Needs to be last to catch all non-existing routes
], ],
declarations: [ declarations: [
AppComponent, AppComponent,
DisablePersonalVaultExportPolicyComponent,
MaximumVaultTimeoutPolicyComponent, MaximumVaultTimeoutPolicyComponent,
DisablePersonalVaultExportPolicyComponent,
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule { }

View File

@@ -1,16 +1,16 @@
import { enableProdMode } from "@angular/core"; import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import "bootstrap"; import 'bootstrap';
import "jquery"; import 'jquery';
import "popper.js"; import 'popper.js';
require("src/scss/styles.scss"); // tslint:disable-next-line
require("src/scss/tailwind.css"); require('src/scss/styles.scss');
import { AppModule } from "./app.module"; import { AppModule } from './app.module';
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === 'production') {
enableProdMode(); enableProdMode();
} }

View File

@@ -1,68 +0,0 @@
import { Directive, Input, OnInit, Self } from "@angular/core";
import { ControlValueAccessor, FormControl, NgControl, Validators } from "@angular/forms";
import { dirtyRequired } from "jslib-angular/validators/dirty.validator";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Directive()
export abstract class BaseCvaComponent implements ControlValueAccessor, OnInit {
get describedById() {
return this.showDescribedBy ? this.controlId + "Desc" : null;
}
get showDescribedBy() {
return this.helperText != null || this.controlDir.control.hasError("required");
}
get isRequired() {
return (
this.controlDir.control.hasValidator(Validators.required) ||
this.controlDir.control.hasValidator(dirtyRequired)
);
}
@Input() label: string;
@Input() controlId: string;
@Input() helperText: string;
internalControl = new FormControl("");
protected onChange: any;
protected onTouched: any;
constructor(@Self() public controlDir: NgControl) {
this.controlDir.valueAccessor = this;
}
ngOnInit() {
this.internalControl.valueChanges.subscribe(this.onValueChangesInternal);
}
onBlurInternal() {
this.onTouched();
}
// CVA interfaces
writeValue(value: string) {
this.internalControl.setValue(value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
if (isDisabled) {
this.internalControl.disable();
} else {
this.internalControl.enable();
}
}
protected onValueChangesInternal: any = (value: string) => this.onChange(value);
// End CVA interfaces
}

View File

@@ -1,16 +0,0 @@
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[formControl]="internalControl"
(blur)="onBlurInternal()"
/>
<label class="form-check-label" [attr.for]="controlId">{{ label }}</label>
</div>
<small *ngIf="showDescribedBy" [attr.id]="describedById" class="form-text text-muted">{{
helperText
}}</small>
</div>

View File

@@ -1,10 +0,0 @@
import { Component } from "@angular/core";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-checkbox",
templateUrl: "input-checkbox.component.html",
})
export class InputCheckboxComponent extends BaseCvaComponent {}

View File

@@ -1,26 +0,0 @@
<div class="form-group">
<label>{{ label }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="controlValue" />
<div class="input-group-append" *ngIf="showLaunch">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launchUri(controlValue)"
>
<i class="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
</button>
</div>
<div class="input-group-append" *ngIf="showCopy">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(controlValue)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>

View File

@@ -1,25 +0,0 @@
import { Component, Input } from "@angular/core";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-text-readonly",
templateUrl: "input-text-readonly.component.html",
})
export class InputTextReadOnlyComponent {
@Input() controlValue: string;
@Input() label: string;
@Input() showCopy = true;
@Input() showLaunch = false;
constructor(private platformUtilsService: PlatformUtilsService) {}
copy(value: string) {
this.platformUtilsService.copyToClipboard(value);
}
launchUri(url: string) {
this.platformUtilsService.launchUri(url);
}
}

View File

@@ -1,33 +0,0 @@
<div class="form-group">
<label [attr.for]="controlId">
{{ label }}
<small *ngIf="isRequired" class="text-muted form-text d-inline"
>({{ "required" | i18n }})</small
>
</label>
<input
[formControl]="internalControl"
class="form-control"
[attr.id]="controlId"
[attr.aria-describedby]="describedById"
[attr.aria-invalid]="controlDir.control.invalid"
(blur)="onBlurInternal()"
/>
<div *ngIf="showDescribedBy" [attr.id]="describedById">
<small
*ngIf="helperText != null && !controlDir.control.hasError(helperTextSameAsError)"
class="form-text text-muted"
>
{{ helperText }}
</small>
<small class="error-inline" *ngIf="controlDir.control.hasError('required')" role="alert">
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{
controlDir.control.hasError(helperTextSameAsError)
? helperText
: ("fieldRequiredError" | i18n: label)
}}
</small>
</div>
</div>

View File

@@ -1,48 +0,0 @@
import { Component, Input, OnInit } from "@angular/core";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-input-text[label][controlId]",
templateUrl: "input-text.component.html",
})
export class InputTextComponent extends BaseCvaComponent implements OnInit {
@Input() helperTextSameAsError: string;
@Input() requiredErrorMessage: string;
@Input() stripSpaces = false;
transformValue: (value: string) => string = null;
ngOnInit() {
super.ngOnInit();
if (this.stripSpaces) {
this.transformValue = this.doStripSpaces;
}
}
writeValue(value: string) {
this.internalControl.setValue(value == null ? "" : value);
}
protected onValueChangesInternal: any = (value: string) => {
let newValue = value;
if (this.transformValue != null) {
newValue = this.transformValue(value);
this.internalControl.setValue(newValue, { emitEvent: false });
}
this.onChange(newValue);
};
protected onValueChangeInternal(value: string) {
let newValue = value;
if (this.transformValue != null) {
newValue = this.transformValue(value);
this.internalControl.setValue(newValue, { emitEvent: false });
}
}
private doStripSpaces(value: string) {
return value.replace(/ /g, "");
}
}

View File

@@ -1,19 +0,0 @@
<div class="form-group">
<label [attr.for]="controlId">
{{ label }}
<small *ngIf="isRequired" class="text-muted form-text d-inline"
>({{ "required" | i18n }})</small
>
</label>
<select
class="form-control"
[attr.id]="controlId"
[attr.aria-invalid]="controlDir.control.invalid"
[formControl]="internalControl"
(blur)="onBlurInternal()"
>
<option *ngFor="let o of selectOptions" [ngValue]="o.value" disabled="{{ o.disabled }}">
{{ o.name }}
</option>
</select>
</div>

View File

@@ -1,14 +0,0 @@
import { Component, Input } from "@angular/core";
import { SelectOptions } from "jslib-angular/interfaces/selectOptions";
import { BaseCvaComponent } from "./base-cva.component";
/** For use in the SSO Config Form only - will be deprecated by the Component Library */
@Component({
selector: "app-select",
templateUrl: "select.component.html",
})
export class SelectComponent extends BaseCvaComponent {
@Input() selectOptions: SelectOptions[];
}

View File

@@ -1,448 +1,363 @@
<div class="page-header d-flex"> <div class="page-header d-flex">
<h1>{{ "singleSignOn" | i18n }}</h1> <h1>{{'singleSignOn' | i18n}}</h1>
</div> </div>
<ng-container *ngIf="loading"> <ng-container *ngIf="loading">
<i <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
<form <form #form (ngSubmit)="submit()" [formGroup]="data" [appApiAction]="formPromise" *ngIf="!loading" ngNativeValidate>
#form
(ngSubmit)="submit()"
[formGroup]="ssoConfigForm"
[appApiAction]="formPromise"
*ngIf="!loading"
>
<p> <p>
{{ "ssoPolicyHelpStart" | i18n }} {{'ssoPolicyHelpStart' | i18n}}
<a routerLink="../policies">{{ "ssoPolicyHelpLink" | i18n }}</a> <a routerLink="../policies">{{'ssoPolicyHelpLink' | i18n}}</a>
{{ "ssoPolicyHelpEnd" | i18n }} {{'ssoPolicyHelpEnd' | i18n}}
<br /> <br>
{{ "ssoPolicyHelpKeyConnector" | i18n }} {{'ssoPolicyHelpKeyConnector' | i18n}}
</p> </p>
<!-- Root form --> <div class="form-group">
<ng-container> <div class="form-check">
<app-input-checkbox <input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
controlId="enabled" <label class="form-check-label" for="enabled">{{'allowSso' | i18n}}</label>
[formControl]="enabled" </div>
[label]="'allowSso' | i18n" <small class="form-text text-muted">{{'allowSsoDesc' | i18n}}</small>
[helperText]="'allowSsoDesc' | i18n" </div>
></app-input-checkbox>
<div class="form-group"> <div class="form-group">
<label>{{ "memberDecryptionOption" | i18n }}</label> <label>{{'memberDecryptionOption' | i18n}}</label>
<div class="form-check form-check-block"> <div class="form-check form-check-block">
<input <input class="form-check-input" type="radio" id="memberDecryptionPass" [value]="false" formControlName="keyConnectorEnabled">
class="form-check-input"
type="radio"
id="memberDecryptionPass"
[value]="false"
formControlName="keyConnectorEnabled"
/>
<label class="form-check-label" for="memberDecryptionPass"> <label class="form-check-label" for="memberDecryptionPass">
{{ "masterPass" | i18n }} {{'masterPass' | i18n}}
<small>{{ "memberDecryptionPassDesc" | i18n }}</small> <small>{{'memberDecryptionPassDesc' | i18n}}</small>
</label> </label>
</div> </div>
<div class="form-check mt-2 form-check-block"> <div class="form-check mt-2 form-check-block">
<input <input class="form-check-input" type="radio" id="memberDecryptionKey" [value]="true" formControlName="keyConnectorEnabled"
class="form-check-input" [attr.disabled]="!organization.useKeyConnector || null">
type="radio"
id="memberDecryptionKey"
[value]="true"
formControlName="keyConnectorEnabled"
[attr.disabled]="!organization.useKeyConnector || null"
/>
<label class="form-check-label" for="memberDecryptionKey"> <label class="form-check-label" for="memberDecryptionKey">
{{ "keyConnector" | i18n }} {{'keyConnector' | i18n}}
<a <a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
target="_blank" href="https://bitwarden.com/help/article/about-key-connector/">
rel="noopener" <i class="fa fa-question-circle-o" aria-hidden="true"></i>
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/about-key-connector/"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a> </a>
<small>{{ "memberDecryptionKeyConnectorDesc" | i18n }}</small> <small>{{'memberDecryptionKeyConnectorDesc' | i18n}}</small>
</label> </label>
</div> </div>
</div> </div>
<!-- Key Connector --> <ng-container *ngIf="data.value.keyConnectorEnabled">
<ng-container *ngIf="ssoConfigForm.get('keyConnectorEnabled').value">
<app-callout type="warning" [useAlertRole]="true"> <app-callout type="warning" [useAlertRole]="true">
{{ "keyConnectorWarning" | i18n }} {{'keyConnectorWarning' | i18n}}
</app-callout> </app-callout>
<div class="form-group"> <div class="form-group">
<label for="keyConnectorUrl"> <label for="keyConnectorUrl">{{'keyConnectorUrl' | i18n}}</label>
{{ "keyConnectorUrl" | i18n }}
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small>
</label>
<div class="input-group"> <div class="input-group">
<input <input class="form-control" formControlName="keyConnectorUrl" id="keyConnectorUrl" required>
class="form-control"
formControlName="keyConnectorUrl"
id="keyConnectorUrl"
aria-describedby="keyConnectorUrlDesc"
(change)="haveTestedKeyConnector = false"
appInputStripSpaces
appA11yInvalid
/>
<div class="input-group-append"> <div class="input-group-append">
<button <button type="button" class="btn btn-outline-secondary" (click)="validateKeyConnectorUrl()"
type="button" [disabled]="!enableTestKeyConnector">
class="btn btn-outline-secondary" <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"
(click)="validateKeyConnectorUrl()" *ngIf="keyConnectorUrl.pending"></i>
[disabled]="!enableTestKeyConnector"
>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="keyConnectorUrl.pending"
></i>
<span *ngIf="!keyConnectorUrl.pending"> <span *ngIf="!keyConnectorUrl.pending">
{{ "keyConnectorTest" | i18n }} {{'keyConnectorTest' | i18n}}
</span> </span>
</button> </button>
</div> </div>
</div> </div>
<div *ngIf="haveTestedKeyConnector" id="keyConnectorUrlDesc" aria-live="polite"> <ng-container *ngIf="keyConnectorUrl.pristine && !keyConnectorUrl.pending">
<small <div class="text-danger" *ngIf="keyConnectorUrl.hasError('invalidUrl')" role="alert">
class="error-inline" <i class="fa fa-exclamation-circle" aria-hidden="true"></i>
*ngIf="keyConnectorUrl.hasError('invalidUrl'); else keyConnectorSuccess" {{'keyConnectorTestFail' | i18n}}
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{ "keyConnectorTestFail" | i18n }}
</small>
<ng-template #keyConnectorSuccess>
<small class="text-success">
<i class="bwi bwi-check-circle" aria-hidden="true"></i>
{{ "keyConnectorTestSuccess" | i18n }}
</small>
</ng-template>
</div> </div>
<div class="text-success" *ngIf="!keyConnectorUrl.hasError('invalidUrl')" role="alert">
<i class="fa fa-check-circle-o" aria-hidden="true"></i>
{{'keyConnectorTestSuccess' | i18n}}
</div>
</ng-container>
</div> </div>
</ng-container> </ng-container>
<app-select <div class="form-group">
controlId="type" <label for="type">{{'type' | i18n}}</label>
[label]="'type' | i18n" <select class="form-control" id="type" formControlName="configType">
[selectOptions]="ssoTypeOptions" <option value="0" disabled>{{'selectType' | i18n}}</option>
formControlName="configType" <option value="1">OpenID Connect</option>
> <option value="2">SAML 2.0</option>
</app-select> </select>
</ng-container> </div>
<!-- OIDC --> <!-- OIDC -->
<div <div *ngIf="data.value.configType == 1">
*ngIf="ssoConfigForm.get('configType').value === ssoType.OpenIdConnect"
[formGroup]="openIdForm"
>
<div class="config-section"> <div class="config-section">
<h2 class="secondary-header">{{ "openIdConnectConfig" | i18n }}</h2> <h2>{{'openIdConnectConfig' | i18n}}</h2>
<div class="form-group">
<app-input-text-readonly <label>{{'callbackPath' | i18n}}</label>
[label]="'callbackPath' | i18n" <div class="input-group">
[controlValue]="callbackPath" <input class="form-control" readonly [value]="callbackPath">
></app-input-text-readonly> <div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
<app-input-text-readonly appA11yTitle="{{'copyValue' | i18n}}"
[label]="'signedOutCallbackPath' | i18n" (click)="copy(callbackPath)">
[controlValue]="signedOutCallbackPath" <i class="fa fa-lg fa-clone" aria-hidden="true"></i>
></app-input-text-readonly>
<app-input-text
[label]="'authority' | i18n"
controlId="authority"
[stripSpaces]="true"
formControlName="authority"
></app-input-text>
<app-input-text
[label]="'clientId' | i18n"
controlId="clientId"
[stripSpaces]="true"
formControlName="clientId"
></app-input-text>
<app-input-text
[label]="'clientSecret' | i18n"
controlId="clientSecret"
[stripSpaces]="true"
formControlName="clientSecret"
></app-input-text>
<app-input-text
[label]="'metadataAddress' | i18n"
controlId="metadataAddress"
[stripSpaces]="true"
[helperText]="'openIdAuthorityRequired' | i18n"
formControlName="metadataAddress"
></app-input-text>
<app-select
controlId="redirectBehavior"
[label]="'oidcRedirectBehavior' | i18n"
[selectOptions]="connectRedirectOptions"
formControlName="redirectBehavior"
>
</app-select>
<app-input-checkbox
controlId="getClaimsFromUserInfoEndpoint"
formControlName="getClaimsFromUserInfoEndpoint"
[label]="'getClaimsFromUserInfoEndpoint' | i18n"
></app-input-checkbox>
<!-- Optional customizations -->
<div
class="section-header d-flex flex-row align-items-center mt-3 mb-3"
(click)="toggleOpenIdCustomizations()"
>
<h3 class="mb-0 mr-2" id="customizations-header">
{{ "openIdOptionalCustomizations" | i18n }}
</h3>
<button
class="mb-1 btn btn-link"
type="button"
appStopClick
role="button"
aria-controls="customizations"
[attr.aria-expanded]="showOpenIdCustomizations"
aria-labelledby="customizations-header"
>
<i
class="bwi"
aria-hidden="true"
[ngClass]="{
'bwi-angle-down': !showOpenIdCustomizations,
'bwi-chevron-up': showOpenIdCustomizations
}"
></i>
</button> </button>
</div> </div>
<div id="customizations" [hidden]="!showOpenIdCustomizations"> </div>
<app-input-text </div>
[label]="'additionalScopes' | i18n" <div class="form-group">
controlId="additionalScopes" <label>{{'signedOutCallbackPath' | i18n}}</label>
[helperText]="'separateMultipleWithComma' | i18n" <div class="input-group">
formControlName="additionalScopes" <input class="form-control" readonly [value]="signedOutCallbackPath">
></app-input-text> <div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
<app-input-text appA11yTitle="{{'copyValue' | i18n}}"
[label]="'additionalUserIdClaimTypes' | i18n" (click)="copy(signedOutCallbackPath)">
controlId="additionalUserIdClaimTypes" <i class="fa fa-lg fa-clone" aria-hidden="true"></i>
[helperText]="'separateMultipleWithComma' | i18n" </button>
formControlName="additionalUserIdClaimTypes" </div>
></app-input-text> </div>
</div>
<app-input-text <div class="form-group">
[label]="'additionalEmailClaimTypes' | i18n" <label for="authority">{{'authority' | i18n}}</label>
controlId="additionalEmailClaimTypes" <input class="form-control" formControlName="authority" id="authority">
[helperText]="'separateMultipleWithComma' | i18n" </div>
formControlName="additionalEmailClaimTypes" <div class="form-group">
></app-input-text> <label for="clientId">{{'clientId' | i18n}}</label>
<input class="form-control" formControlName="clientId" id="clientId">
<app-input-text </div>
[label]="'additionalNameClaimTypes' | i18n" <div class="form-group">
controlId="additionalNameClaimTypes" <label for="clientSecret">{{'clientSecret' | i18n}}</label>
[helperText]="'separateMultipleWithComma' | i18n" <input class="form-control" formControlName="clientSecret" id="clientSecret">
formControlName="additionalNameClaimTypes" </div>
></app-input-text> <div class="form-group">
<label for="metadataAddress">{{'metadataAddress' | i18n}}</label>
<app-input-text <input class="form-control" formControlName="metadataAddress" id="metadataAddress">
[label]="'acrValues' | i18n" </div>
controlId="acrValues" <div class="form-group">
helperText="acr_values" <label for="redirectBehavior">{{'oidcRedirectBehavior' | i18n}}</label>
formControlName="acrValues" <select class="form-control" formControlName="redirectBehavior" id="redirectBehavior">
></app-input-text> <option value="0">Redirect GET</option>
<option value="1">Form POST</option>
<app-input-text </select>
[label]="'expectedReturnAcrValue' | i18n" </div>
controlId="expectedReturnAcrValue" <div class="form-group">
helperText="acr_validation" <div class="form-check">
formControlName="expectedReturnAcrValue" <input class="form-check-input" type="checkbox" id="getClaimsFromUserInfoEndpoint"
></app-input-text> formControlName="getClaimsFromUserInfoEndpoint">
<label class="form-check-label" for="getClaimsFromUserInfoEndpoint">
{{'getClaimsFromUserInfoEndpoint' | i18n}}
</label>
</div>
</div>
<div class="form-group">
<label for="additionalScopes">{{'additionalScopes' | i18n}}</label>
<input class="form-control" formControlName="additionalScopes" id="additionalScopes">
</div>
<div class="form-group">
<label for="additionalUserIdClaimTypes">{{'additionalUserIdClaimTypes' | i18n}}</label>
<input class="form-control" formControlName="additionalUserIdClaimTypes"
id="additionalUserIdClaimTypes">
</div>
<div class="form-group">
<label for="additionalEmailClaimTypes">{{'additionalEmailClaimTypes' | i18n}}</label>
<input class="form-control" formControlName="additionalEmailClaimTypes"
id="additionalEmailClaimTypes">
</div>
<div class="form-group">
<label for="additionalNameClaimTypes">{{'additionalNameClaimTypes' | i18n}}</label>
<input class="form-control" formControlName="additionalNameClaimTypes"
id="additionalNameClaimTypes">
</div>
<div class="form-group">
<label for="acrValues">{{'acrValues' | i18n}}</label>
<input class="form-control" formControlName="acrValues" id="acrValues">
</div>
<div class="form-group">
<label for="expectedReturnAcrValue">{{'expectedReturnAcrValue' | i18n}}</label>
<input class="form-control" formControlName="expectedReturnAcrValue" id="expectedReturnAcrValue">
</div> </div>
</div> </div>
</div> </div>
<!-- SAML2 SP --> <div *ngIf="data.value.configType == 2">
<div *ngIf="ssoConfigForm.get('configType').value === ssoType.Saml2" [formGroup]="samlForm">
<!-- SAML2 SP --> <!-- SAML2 SP -->
<div class="config-section"> <div class="config-section">
<h2 class="secondary-header">{{ "samlSpConfig" | i18n }}</h2> <h2>{{'samlSpConfig' | i18n}}</h2>
<div class="form-group">
<app-input-text-readonly <label>{{'spEntityId' | i18n}}</label>
[label]="'spEntityId' | i18n" <div class="input-group">
[controlValue]="spEntityId" <input class="form-control" readonly [value]="spEntityId" >
></app-input-text-readonly> <div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
<app-input-text-readonly appA11yTitle="{{'copyValue' | i18n}}"
[label]="'spMetadataUrl' | i18n" (click)="copy(spEntityId)">
[controlValue]="spMetadataUrl" <i class="fa fa-lg fa-clone" aria-hidden="true"></i>
[showLaunch]="true" </button>
></app-input-text-readonly> </div>
</div>
<app-input-text-readonly </div>
[label]="'spAcsUrl' | i18n" <div class="form-group">
[controlValue]="spAcsUrl" <label>{{'spMetadataUrl' | i18n}}</label>
></app-input-text-readonly> <div class="input-group">
<input class="form-control" readonly [value]="spMetadataUrl">
<app-select <div class="input-group-append">
controlId="spNameIdFormat" <button type="button" class="btn btn-outline-secondary"
[label]="'spNameIdFormat' | i18n" appA11yTitle="{{'launch' | i18n}}"
[selectOptions]="saml2NameIdFormatOptions" (click)="launchUri(spMetadataUrl)">
formControlName="spNameIdFormat" <i class="fa fa-lg fa-external-link" aria-hidden="true"></i>
> </button>
</app-select> <button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyValue' | i18n}}"
<app-select (click)="copy(spMetadataUrl)">
controlId="spOutboundSigningAlgorithm" <i class="fa fa-lg fa-clone" aria-hidden="true"></i>
[label]="'spOutboundSigningAlgorithm' | i18n" </button>
[selectOptions]="samlSigningAlgorithmOptions" </div>
formControlName="spOutboundSigningAlgorithm" </div>
> </div>
</app-select> <div class="form-group">
<label>{{'spAcsUrl' | i18n}}</label>
<app-select <div class="input-group">
controlId="spSigningBehavior" <input class="form-control" readonly [value]="spAcsUrl">
[label]="'spSigningBehavior' | i18n" <div class="input-group-append">
[selectOptions]="saml2SigningBehaviourOptions" <button type="button" class="btn btn-outline-secondary"
formControlName="spSigningBehavior" appA11yTitle="{{'copyValue' | i18n}}"
> (click)="copy(spAcsUrl)">
</app-select> <i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
<app-select </div>
controlId="spMinIncomingSigningAlgorithm" </div>
[label]="'spMinIncomingSigningAlgorithm' | i18n" </div>
[selectOptions]="samlSigningAlgorithmOptions" <div class="form-group">
formControlName="spMinIncomingSigningAlgorithm" <label for="spNameIdFormat">{{'spNameIdFormat' | i18n}}</label>
> <select class="form-control" formControlName="spNameIdFormat" id="spNameIdFormat">
</app-select> <option value="0">Not Configured</option>
<option value="1">Unspecified</option>
<app-input-checkbox <option value="2">Email Address</option>
controlId="spWantAssertionsSigned" <option value="3">X.509 Subject Name</option>
formControlName="spWantAssertionsSigned" <option value="4">Windows Domain Qualified Name</option>
[label]="'spWantAssertionsSigned' | i18n" <option value="5">Kerberos Principal Name</option>
></app-input-checkbox> <option value="6">Entity Identifier</option>
<option value="7">Persistent</option>
<app-input-checkbox <option value="8">Transient</option>
controlId="spValidateCertificates" </select>
formControlName="spValidateCertificates" </div>
[label]="'spValidateCertificates' | i18n" <div class="form-group">
></app-input-checkbox> <label for="spOutboundSigningAlgorithm">{{'spOutboundSigningAlgorithm' | i18n}}</label>
<select class="form-control" formControlName="spOutboundSigningAlgorithm"
id="spOutboundSigningAlgorithm">
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
</select>
</div>
<div class="form-group">
<label for="spSigningBehavior">{{'spSigningBehavior' | i18n}}</label>
<select class="form-control" formControlName="spSigningBehavior" id="spSigningBehavior">
<option value="0">If IdP Wants Authn Requests Signed</option>
<option value="1">Always</option>
<option value="3">Never</option>
</select>
</div>
<div class="form-group">
<label for="spMinIncomingSigningAlgorithm">{{'spMinIncomingSigningAlgorithm' | i18n}}</label>
<select class="form-control" formControlName="spMinIncomingSigningAlgorithm"
id="spMinIncomingSigningAlgorithm">
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
</select>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="spWantAssertionsSigned"
formControlName="spWantAssertionsSigned">
<label class="form-check-label" for="spWantAssertionsSigned">
{{'spWantAssertionsSigned' | i18n}}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="spValidateCertificates"
formControlName="spValidateCertificates">
<label class="form-check-label" for="spValidateCertificates">
{{'spValidateCertificates' | i18n}}
</label>
</div>
</div>
</div> </div>
<!-- SAML2 IDP --> <!-- SAML2 IDP -->
<div class="config-section"> <div class="config-section">
<h2 class="secondary-header">{{ "samlIdpConfig" | i18n }}</h2> <h2>{{'samlIdpConfig' | i18n}}</h2>
<app-input-text
[label]="'idpEntityId' | i18n"
controlId="idpEntityId"
formControlName="idpEntityId"
></app-input-text>
<app-select
controlId="idpBindingType"
[label]="'idpBindingType' | i18n"
[selectOptions]="saml2BindingTypeOptions"
formControlName="idpBindingType"
>
</app-select>
<app-input-text
[label]="'idpSingleSignOnServiceUrl' | i18n"
controlId="idpSingleSignOnServiceUrl"
[helperText]="'idpSingleSignOnServiceUrlRequired' | i18n"
[stripSpaces]="true"
formControlName="idpSingleSignOnServiceUrl"
></app-input-text>
<app-input-text
[label]="'idpSingleLogoutServiceUrl' | i18n"
controlId="idpSingleLogoutServiceUrl"
[stripSpaces]="true"
formControlName="idpSingleLogoutServiceUrl"
></app-input-text>
<div class="form-group"> <div class="form-group">
<label for="idpX509PublicCert"> <label for="idpEntityId">{{'idpEntityId' | i18n}}</label>
{{ "idpX509PublicCert" | i18n }} <input class="form-control" formControlName="idpEntityId" id="idpEntityId">
<small class="text-muted form-text d-inline">({{ "required" | i18n }})</small> </div>
</label> <div class="form-group">
<textarea <label for="idpBindingType">{{'idpBindingType' | i18n}}</label>
formControlName="idpX509PublicCert" <select class="form-control" formControlName="idpBindingType" id="idpBindingType">
class="form-control form-control-sm text-monospace" <option value="1">Redirect</option>
rows="6" <option value="2">HTTP POST</option>
id="idpX509PublicCert" <option value="4">Artifact</option>
appA11yInvalid </select>
aria-describedby="idpX509PublicCertDesc" </div>
></textarea> <div class="form-group">
<small <label for="idpSingleSignOnServiceUrl">{{'idpSingleSignOnServiceUrl' | i18n}}</label>
id="idpX509PublicCertDesc" <input class="form-control" formControlName="idpSingleSignOnServiceUrl" id="idpSingleSignOnServiceUrl">
class="error-inline" </div>
role="alert" <div class="form-group">
*ngIf="samlForm.get('idpX509PublicCert').hasError('required')" <label for="idpSingleLogoutServiceUrl">{{'idpSingleLogoutServiceUrl' | i18n}}</label>
> <input class="form-control" formControlName="idpSingleLogoutServiceUrl" id="idpSingleLogoutServiceUrl">
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i> </div>
<span class="sr-only">{{ "error" | i18n }}:</span> <div class="form-group">
{{ "fieldRequiredError" | i18n: ("idpX509PublicCert" | i18n) }} <label for="idpArtifactResolutionServiceUrl">{{'idpArtifactResolutionServiceUrl' | i18n}}</label>
</small> <input class="form-control" formControlName="idpArtifactResolutionServiceUrl"
id="idpArtifactResolutionServiceUrl">
</div>
<div class="form-group">
<label for="idpX509PublicCert">{{'idpX509PublicCert' | i18n}}</label>
<textarea formControlName="idpX509PublicCert" class="form-control form-control-sm text-monospace"
rows="6" id="idpX509PublicCert"></textarea>
</div>
<div class="form-group">
<label for="idpOutboundSigningAlgorithm">{{'idpOutboundSigningAlgorithm' | i18n}}</label>
<select class="form-control" formControlName="idpOutboundSigningAlgorithm"
id="idpOutboundSigningAlgorithm">
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
</select>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="idpAllowUnsolicitedAuthnResponse"
formControlName="idpAllowUnsolicitedAuthnResponse">
<label class="form-check-label" for="idpAllowUnsolicitedAuthnResponse">
{{'idpAllowUnsolicitedAuthnResponse' | i18n}}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="idpDisableOutboundLogoutRequests"
formControlName="idpDisableOutboundLogoutRequests">
<label class="form-check-label" for="idpDisableOutboundLogoutRequests">
{{'idpDisableOutboundLogoutRequests' | i18n}}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="idpWantAuthnRequestsSigned"
formControlName="idpWantAuthnRequestsSigned">
<label class="form-check-label" for="idpWantAuthnRequestsSigned">
{{'idpWantAuthnRequestsSigned' | i18n}}
</label>
</div>
</div> </div>
<app-select
controlId="idpOutboundSigningAlgorithm"
[label]="'idpOutboundSigningAlgorithm' | i18n"
[selectOptions]="samlSigningAlgorithmOptions"
formControlName="idpOutboundSigningAlgorithm"
>
</app-select>
<!--TODO: Uncomment once Unsolicited IdP Response is supported-->
<!-- <app-input-checkbox
controlId="idpAllowUnsolicitedAuthnResponse"
formControlName="idpAllowUnsolicitedAuthnResponse"
[label]="'idpAllowUnsolicitedAuthnResponse' | i18n"
></app-input-checkbox> -->
<app-input-checkbox
controlId="idpAllowOutboundLogoutRequests"
formControlName="idpAllowOutboundLogoutRequests"
[label]="'idpAllowOutboundLogoutRequests' | i18n"
></app-input-checkbox>
<app-input-checkbox
controlId="idpWantAuthnRequestsSigned"
formControlName="idpWantAuthnRequestsSigned"
[label]="'idpSignAuthenticationRequests' | i18n"
></app-input-checkbox>
</div> </div>
</div> </div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span> <span>{{'save' | i18n}}</span>
</button> </button>
<div
id="errorSummary"
class="error-summary text-danger"
*ngIf="this.getErrorCount(ssoConfigForm) as errorCount"
>
<i class="bwi bwi-exclamation-circle" aria-hidden="true"></i>
<span class="sr-only">{{ "error" | i18n }}:</span>
{{
(errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural") | i18n: errorCount
}}
</div>
</form> </form>

View File

@@ -1,82 +1,33 @@
import { Component, OnInit } from "@angular/core";
import { AbstractControl, FormBuilder, FormGroup } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { SelectOptions } from "jslib-angular/interfaces/selectOptions";
import { dirtyRequired } from "jslib-angular/validators/dirty.validator";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { import {
OpenIdConnectRedirectBehavior, Component,
Saml2BindingType, OnInit,
Saml2NameIdFormat, } from '@angular/core';
Saml2SigningBehavior, import { FormBuilder } from '@angular/forms';
SsoType, import { ActivatedRoute } from '@angular/router';
} from "jslib-common/enums/ssoEnums";
import { Utils } from "jslib-common/misc/utils";
import { SsoConfigApi } from "jslib-common/models/api/ssoConfigApi";
import { Organization } from "jslib-common/models/domain/organization";
import { OrganizationSsoRequest } from "jslib-common/models/request/organization/organizationSsoRequest";
import { OrganizationSsoResponse } from "jslib-common/models/response/organization/organizationSsoResponse";
import { SsoConfigView } from "jslib-common/models/view/ssoConfigView";
const defaultSigningAlgorithm = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { Organization } from 'jslib-common/models/domain/organization';
import { OrganizationSsoRequest } from 'jslib-common/models/request/organization/organizationSsoRequest';
@Component({ @Component({
selector: "app-org-manage-sso", selector: 'app-org-manage-sso',
templateUrl: "sso.component.html", templateUrl: 'sso.component.html',
}) })
export class SsoComponent implements OnInit { export class SsoComponent implements OnInit {
readonly ssoType = SsoType;
readonly ssoTypeOptions: SelectOptions[] = [ samlSigningAlgorithms = [
{ name: this.i18nService.t("selectType"), value: SsoType.None, disabled: true }, 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
{ name: "OpenID Connect", value: SsoType.OpenIdConnect }, 'http://www.w3.org/2000/09/xmldsig#rsa-sha384',
{ name: "SAML 2.0", value: SsoType.Saml2 }, 'http://www.w3.org/2000/09/xmldsig#rsa-sha512',
'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
]; ];
readonly samlSigningAlgorithms = [
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"http://www.w3.org/2000/09/xmldsig#rsa-sha384",
"http://www.w3.org/2000/09/xmldsig#rsa-sha512",
"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
];
readonly saml2SigningBehaviourOptions: SelectOptions[] = [
{
name: "If IdP Wants Authn Requests Signed",
value: Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned,
},
{ name: "Always", value: Saml2SigningBehavior.Always },
{ name: "Never", value: Saml2SigningBehavior.Never },
];
readonly saml2BindingTypeOptions: SelectOptions[] = [
{ name: "Redirect", value: Saml2BindingType.HttpRedirect },
{ name: "HTTP POST", value: Saml2BindingType.HttpPost },
];
readonly saml2NameIdFormatOptions: SelectOptions[] = [
{ name: "Not Configured", value: Saml2NameIdFormat.NotConfigured },
{ name: "Unspecified", value: Saml2NameIdFormat.Unspecified },
{ name: "Email Address", value: Saml2NameIdFormat.EmailAddress },
{ name: "X.509 Subject Name", value: Saml2NameIdFormat.X509SubjectName },
{ name: "Windows Domain Qualified Name", value: Saml2NameIdFormat.WindowsDomainQualifiedName },
{ name: "Kerberos Principal Name", value: Saml2NameIdFormat.KerberosPrincipalName },
{ name: "Entity Identifier", value: Saml2NameIdFormat.EntityIdentifier },
{ name: "Persistent", value: Saml2NameIdFormat.Persistent },
{ name: "Transient", value: Saml2NameIdFormat.Transient },
];
readonly connectRedirectOptions: SelectOptions[] = [
{ name: "Redirect GET", value: OpenIdConnectRedirectBehavior.RedirectGet },
{ name: "Form POST", value: OpenIdConnectRedirectBehavior.FormPost },
];
showOpenIdCustomizations = false;
loading = true; loading = true;
haveTestedKeyConnector = false;
organizationId: string; organizationId: string;
organization: Organization; organization: Organization;
formPromise: Promise<any>; formPromise: Promise<any>;
@@ -87,15 +38,19 @@ export class SsoComponent implements OnInit {
spMetadataUrl: string; spMetadataUrl: string;
spAcsUrl: string; spAcsUrl: string;
enabled = this.formBuilder.control(false); enabled = this.fb.control(false);
data = this.fb.group({
configType: [],
openIdForm = this.formBuilder.group( keyConnectorEnabled: [],
{ keyConnectorUrl: [],
authority: ["", dirtyRequired],
clientId: ["", dirtyRequired], // OpenId
clientSecret: ["", dirtyRequired], authority: [],
clientId: [],
clientSecret: [],
metadataAddress: [], metadataAddress: [],
redirectBehavior: [OpenIdConnectRedirectBehavior.RedirectGet, dirtyRequired], redirectBehavior: [],
getClaimsFromUserInfoEndpoint: [], getClaimsFromUserInfoEndpoint: [],
additionalScopes: [], additionalScopes: [],
additionalUserIdClaimTypes: [], additionalUserIdClaimTypes: [],
@@ -103,83 +58,44 @@ export class SsoComponent implements OnInit {
additionalNameClaimTypes: [], additionalNameClaimTypes: [],
acrValues: [], acrValues: [],
expectedReturnAcrValue: [], expectedReturnAcrValue: [],
},
{
updateOn: "blur",
}
);
samlForm = this.formBuilder.group( // SAML
{ spNameIdFormat: [],
spNameIdFormat: [Saml2NameIdFormat.NotConfigured], spOutboundSigningAlgorithm: [],
spOutboundSigningAlgorithm: [defaultSigningAlgorithm], spSigningBehavior: [],
spSigningBehavior: [Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned], spMinIncomingSigningAlgorithm: [],
spMinIncomingSigningAlgorithm: [defaultSigningAlgorithm],
spWantAssertionsSigned: [], spWantAssertionsSigned: [],
spValidateCertificates: [], spValidateCertificates: [],
idpEntityId: ["", dirtyRequired], idpEntityId: [],
idpBindingType: [Saml2BindingType.HttpRedirect], idpBindingType: [],
idpSingleSignOnServiceUrl: [], idpSingleSignOnServiceUrl: [],
idpSingleLogoutServiceUrl: [], idpSingleLogoutServiceUrl: [],
idpX509PublicCert: ["", dirtyRequired], idpArtifactResolutionServiceUrl: [],
idpOutboundSigningAlgorithm: [defaultSigningAlgorithm], idpX509PublicCert: [],
idpOutboundSigningAlgorithm: [],
idpAllowUnsolicitedAuthnResponse: [], idpAllowUnsolicitedAuthnResponse: [],
idpAllowOutboundLogoutRequests: [true], idpDisableOutboundLogoutRequests: [],
idpWantAuthnRequestsSigned: [], idpWantAuthnRequestsSigned: [],
},
{
updateOn: "blur",
}
);
ssoConfigForm = this.formBuilder.group({
configType: [SsoType.None],
keyConnectorEnabled: [false],
keyConnectorUrl: [""],
openId: this.openIdForm,
saml: this.samlForm,
}); });
constructor( constructor(private fb: FormBuilder, private route: ActivatedRoute, private apiService: ApiService,
private formBuilder: FormBuilder, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private route: ActivatedRoute, private userService: UserService) { }
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private organizationService: OrganizationService
) {}
async ngOnInit() { async ngOnInit() {
this.ssoConfigForm.get("configType").valueChanges.subscribe((newType: SsoType) => { this.route.parent.parent.params.subscribe(async params => {
if (newType === SsoType.OpenIdConnect) {
this.openIdForm.enable();
this.samlForm.disable();
} else if (newType === SsoType.Saml2) {
this.openIdForm.disable();
this.samlForm.enable();
} else {
this.openIdForm.disable();
this.samlForm.disable();
}
});
this.samlForm
.get("spSigningBehavior")
.valueChanges.subscribe(() =>
this.samlForm.get("idpX509PublicCert").updateValueAndValidity()
);
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId; this.organizationId = params.organizationId;
await this.load(); await this.load();
}); });
} }
async load() { async load() {
this.organization = await this.organizationService.get(this.organizationId); this.organization = await this.userService.getOrganization(this.organizationId);
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId); const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
this.populateForm(ssoSettings);
this.data.patchValue(ssoSettings.data);
this.enabled.setValue(ssoSettings.enabled);
this.callbackPath = ssoSettings.urls.callbackPath; this.callbackPath = ssoSettings.urls.callbackPath;
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath; this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
@@ -187,31 +103,29 @@ export class SsoComponent implements OnInit {
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl; this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
this.spAcsUrl = ssoSettings.urls.spAcsUrl; this.spAcsUrl = ssoSettings.urls.spAcsUrl;
this.keyConnectorUrl.markAsDirty();
this.loading = false; this.loading = false;
} }
copy(value: string) {
this.platformUtilsService.copyToClipboard(value);
}
launchUri(url: string) {
this.platformUtilsService.launchUri(url);
}
async submit() { async submit() {
this.validateForm(this.ssoConfigForm); this.formPromise = this.postData();
if (this.ssoConfigForm.get("keyConnectorEnabled").value) {
await this.validateKeyConnectorUrl();
}
if (!this.ssoConfigForm.valid) {
this.readOutErrors();
return;
}
const request = new OrganizationSsoRequest();
request.enabled = this.enabled.value;
request.data = SsoConfigApi.fromView(this.ssoConfigForm.value as SsoConfigView);
this.formPromise = this.apiService.postOrganizationSso(this.organizationId, request);
try { try {
const response = await this.formPromise; const response = await this.formPromise;
this.populateForm(response);
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved")); this.data.patchValue(response.data);
this.enabled.setValue(response.enabled);
this.platformUtilsService.showToast('success', null, this.i18nService.t('ssoSettingsSaved'));
} catch { } catch {
// Logged by appApiAction, do nothing // Logged by appApiAction, do nothing
} }
@@ -219,8 +133,24 @@ export class SsoComponent implements OnInit {
this.formPromise = null; this.formPromise = null;
} }
async postData() {
if (this.data.get('keyConnectorEnabled').value) {
await this.validateKeyConnectorUrl();
if (this.keyConnectorUrl.hasError('invalidUrl')) {
throw new Error(this.i18nService.t('keyConnectorTestFail'));
}
}
const request = new OrganizationSsoRequest();
request.enabled = this.enabled.value;
request.data = this.data.value;
return this.apiService.postOrganizationSso(this.organizationId, request);
}
async validateKeyConnectorUrl() { async validateKeyConnectorUrl() {
if (this.haveTestedKeyConnector) { if (this.keyConnectorUrl.pristine) {
return; return;
} }
@@ -235,84 +165,16 @@ export class SsoComponent implements OnInit {
}); });
} }
this.haveTestedKeyConnector = true; this.keyConnectorUrl.markAsPristine();
}
toggleOpenIdCustomizations() {
this.showOpenIdCustomizations = !this.showOpenIdCustomizations;
}
getErrorCount(form: FormGroup): number {
return Object.values(form.controls).reduce((acc: number, control: AbstractControl) => {
if (control instanceof FormGroup) {
return acc + this.getErrorCount(control);
}
if (control.errors == null) {
return acc;
}
return acc + Object.keys(control.errors).length;
}, 0);
} }
get enableTestKeyConnector() { get enableTestKeyConnector() {
return ( return this.data.get('keyConnectorEnabled').value &&
this.ssoConfigForm.get("keyConnectorEnabled").value && this.keyConnectorUrl != null &&
!Utils.isNullOrWhitespace(this.keyConnectorUrl?.value) this.keyConnectorUrl.value !== '';
);
} }
get keyConnectorUrl() { get keyConnectorUrl() {
return this.ssoConfigForm.get("keyConnectorUrl"); return this.data.get('keyConnectorUrl');
}
get samlSigningAlgorithmOptions(): SelectOptions[] {
return this.samlSigningAlgorithms.map((algorithm) => ({ name: algorithm, value: algorithm }));
}
private validateForm(form: FormGroup) {
Object.values(form.controls).forEach((control: AbstractControl) => {
if (control.disabled) {
return;
}
if (control instanceof FormGroup) {
this.validateForm(control);
} else {
control.markAsDirty();
control.markAsTouched();
control.updateValueAndValidity();
}
});
}
private populateForm(ssoSettings: OrganizationSsoResponse) {
this.enabled.setValue(ssoSettings.enabled);
if (ssoSettings.data != null) {
const ssoConfigView = new SsoConfigView(ssoSettings.data);
this.ssoConfigForm.patchValue(ssoConfigView);
}
}
private readOutErrors() {
const errorText = this.i18nService.t("error");
const errorCount = this.getErrorCount(this.ssoConfigForm);
const errorCountText = this.i18nService.t(
errorCount === 1 ? "formErrorSummarySingle" : "formErrorSummaryPlural",
errorCount.toString()
);
const div = document.createElement("div");
div.className = "sr-only";
div.id = "srErrorCount";
div.setAttribute("aria-live", "polite");
div.innerText = errorText + ": " + errorCountText;
const existing = document.getElementById("srErrorCount");
if (existing != null) {
existing.remove();
}
document.body.append(div);
} }
} }

View File

@@ -1,34 +1,44 @@
import { NgModule } from "@angular/core"; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from "jslib-angular/guards/auth.guard"; import { AuthGuardService } from 'jslib-angular/services/auth-guard.service';
import { Permissions } from "jslib-common/enums/permissions";
import { PermissionsGuard } from "src/app/organizations/guards/permissions.guard"; import { Permissions } from 'jslib-common/enums/permissions';
import { OrganizationLayoutComponent } from "src/app/organizations/layouts/organization-layout.component";
import { ManageComponent } from "src/app/organizations/manage/manage.component";
import { NavigationPermissionsService } from "src/app/organizations/services/navigation-permissions.service";
import { SsoComponent } from "./manage/sso.component"; import { OrganizationLayoutComponent } from 'src/app/layouts/organization-layout.component';
import { ManageComponent } from 'src/app/organizations/manage/manage.component';
import { OrganizationGuardService } from 'src/app/services/organization-guard.service';
import { OrganizationTypeGuardService } from 'src/app/services/organization-type-guard.service';
import { SsoComponent } from './manage/sso.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: "organizations/:organizationId", path: 'organizations/:organizationId',
component: OrganizationLayoutComponent, component: OrganizationLayoutComponent,
canActivate: [AuthGuard, PermissionsGuard], canActivate: [AuthGuardService, OrganizationGuardService],
children: [ children: [
{ {
path: "manage", path: 'manage',
component: ManageComponent, component: ManageComponent,
canActivate: [PermissionsGuard], canActivate: [OrganizationTypeGuardService],
data: { data: {
permissions: NavigationPermissionsService.getPermissions("manage").concat( permissions: [
Permissions.ManageSso Permissions.CreateNewCollections,
), Permissions.EditAnyCollection,
Permissions.DeleteAnyCollection,
Permissions.EditAssignedCollections,
Permissions.DeleteAssignedCollections,
Permissions.AccessEventLogs,
Permissions.ManageGroups,
Permissions.ManageUsers,
Permissions.ManagePolicies,
Permissions.ManageSso,
],
}, },
children: [ children: [
{ {
path: "sso", path: 'sso',
component: SsoComponent, component: SsoComponent,
}, },
], ],
@@ -41,4 +51,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],
}) })
export class OrganizationsRoutingModule {} export class OrganizationsRoutingModule { }

View File

@@ -1,31 +1,21 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from '@angular/common';
import { NgModule } from "@angular/core"; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { JslibModule } from "jslib-angular/jslib.module"; import { OssModule } from 'src/app/oss.module';
import { InputCheckboxComponent } from "./components/input-checkbox.component"; import { SsoComponent } from './manage/sso.component';
import { InputTextReadOnlyComponent } from "./components/input-text-readonly.component"; import { OrganizationsRoutingModule } from './organizations-routing.module';
import { InputTextComponent } from "./components/input-text.component";
import { SelectComponent } from "./components/select.component";
import { SsoComponent } from "./manage/sso.component";
import { OrganizationsRoutingModule } from "./organizations-routing.module";
// Form components are for use in the SSO Configuration Form only and should not be exported for use elsewhere.
// They will be deprecated by the Component Library.
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
JslibModule, OssModule,
OrganizationsRoutingModule, OrganizationsRoutingModule,
], ],
declarations: [ declarations: [
InputCheckboxComponent,
InputTextComponent,
InputTextReadOnlyComponent,
SelectComponent,
SsoComponent, SsoComponent,
], ],
}) })

View File

@@ -1,12 +1,6 @@
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
class="form-check-input" <label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
</div> </div>
</div> </div>

View File

@@ -1,21 +1,24 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { PolicyType } from "jslib-common/enums/policyType"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { import { PolicyType } from 'jslib-common/enums/policyType';
BasePolicy,
BasePolicyComponent, import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
} from "src/app/organizations/policies/base-policy.component";
import { BasePolicy, BasePolicyComponent } from 'src/app/organizations/policies/base-policy.component';
export class DisablePersonalVaultExportPolicy extends BasePolicy { export class DisablePersonalVaultExportPolicy extends BasePolicy {
name = "disablePersonalVaultExport"; name = 'disablePersonalVaultExport';
description = "disablePersonalVaultExportDesc"; description = 'disablePersonalVaultExportDesc';
type = PolicyType.DisablePersonalVaultExport; type = PolicyType.DisablePersonalVaultExport;
component = DisablePersonalVaultExportPolicyComponent; component = DisablePersonalVaultExportPolicyComponent;
} }
@Component({ @Component({
selector: "policy-disable-personal-vault-export", selector: 'policy-disable-personal-vault-export',
templateUrl: "disable-personal-vault-export.component.html", templateUrl: 'disable-personal-vault-export.component.html',
}) })
export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {} export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {
}

View File

@@ -1,46 +1,26 @@
<app-callout type="tip" title="{{ 'prerequisite' | i18n }}"> <app-callout type="tip" title="{{'prerequisite' | i18n}}">
{{ "requireSsoPolicyReq" | i18n }} {{'requireSsoPolicyReq' | i18n}}
</app-callout> </app-callout>
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
class="form-check-input" <label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
</div> </div>
</div> </div>
<div [formGroup]="data"> <div [formGroup]="data">
<div class="form-group"> <div class="form-group">
<label for="hours">{{ "maximumVaultTimeoutLabel" | i18n }}</label> <label for="hours">{{'maximumVaultTimeoutLabel' | i18n}}</label>
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<input <input id="hours" class="form-control" type="number" min="0" name="hours" formControlName="hours">
id="hours" <small>{{'hours' | i18n }}</small>
class="form-control"
type="number"
min="0"
name="hours"
formControlName="hours"
/>
<small>{{ "hours" | i18n }}</small>
</div> </div>
<div class="col-6"> <div class="col-6">
<input <input id="minutes" class="form-control" type="number" min="0" max="59" name="minutes"
id="minutes" formControlName="minutes">
class="form-control" <small>{{'minutes' | i18n }}</small>
type="number"
min="0"
max="59"
name="minutes"
formControlName="minutes"
/>
<small>{{ "minutes" | i18n }}</small>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,33 +1,33 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { FormBuilder } from "@angular/forms"; import { FormBuilder } from '@angular/forms';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PolicyType } from "jslib-common/enums/policyType";
import { PolicyRequest } from "jslib-common/models/request/policyRequest";
import { import { PolicyType } from 'jslib-common/enums/policyType';
BasePolicy,
BasePolicyComponent, import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
} from "src/app/organizations/policies/base-policy.component";
import { BasePolicy, BasePolicyComponent } from 'src/app/organizations/policies/base-policy.component';
export class MaximumVaultTimeoutPolicy extends BasePolicy { export class MaximumVaultTimeoutPolicy extends BasePolicy {
name = "maximumVaultTimeout"; name = 'maximumVaultTimeout';
description = "maximumVaultTimeoutDesc"; description = 'maximumVaultTimeoutDesc';
type = PolicyType.MaximumVaultTimeout; type = PolicyType.MaximumVaultTimeout;
component = MaximumVaultTimeoutPolicyComponent; component = MaximumVaultTimeoutPolicyComponent;
} }
@Component({ @Component({
selector: "policy-maximum-timeout", selector: 'policy-maximum-timeout',
templateUrl: "maximum-vault-timeout.component.html", templateUrl: 'maximum-vault-timeout.component.html',
}) })
export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent { export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
data = this.formBuilder.group({
data = this.fb.group({
hours: [null], hours: [null],
minutes: [null], minutes: [null],
}); });
constructor(private formBuilder: FormBuilder, private i18nService: I18nService) { constructor(private fb: FormBuilder, private i18nService: I18nService) {
super(); super();
} }
@@ -57,12 +57,12 @@ export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> { buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false; const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
if (this.enabled.value && !singleOrgEnabled) { if (this.enabled.value && !singleOrgEnabled) {
throw new Error(this.i18nService.t("requireSsoPolicyReqError")); throw new Error(this.i18nService.t('requireSsoPolicyReqError'));
} }
const data = this.buildRequestData(); const data = this.buildRequestData();
if (data?.minutes == null || data?.minutes <= 0) { if (data?.minutes == null || data?.minutes <= 0) {
throw new Error(this.i18nService.t("invalidMaximumVaultTimeout")); throw new Error(this.i18nService.t('invalidMaximumVaultTimeout'));
} }
return super.buildRequest(policiesEnabledMap); return super.buildRequest(policiesEnabledMap);

View File

@@ -3,21 +3,16 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title" id="addTitle"> <h2 class="modal-title" id="addTitle">
{{ "addExistingOrganization" | i18n }} {{'addExistingOrganization' | i18n}}
</h2> </h2>
<button <button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="card-body text-center" *ngIf="loading"> <div class="card-body text-center" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
{{ "loading" | i18n }} {{'loading' | i18n}}
</div> </div>
<ng-container *ngIf="!loading"> <ng-container *ngIf="!loading">
<table class="table table-hover table-list"> <table class="table table-hover table-list">
@@ -26,16 +21,10 @@
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar> <app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td> </td>
<td> <td>
{{ o.name }} {{o.name}}
</td> </td>
<td> <td>
<button <button class="btn btn-outline-secondary pull-right" (click)="add(o)" [disabled]="formPromise">Add</button>
class="btn btn-outline-secondary pull-right"
(click)="add(o)"
[disabled]="formPromise"
>
Add
</button>
</td> </td>
</tr> </tr>
</table> </table>

View File

@@ -1,19 +1,32 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import {
Component,
EventEmitter,
Input,
OnInit,
Output
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { ValidationService } from "jslib-angular/services/validation.service"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ProviderService } from "jslib-common/abstractions/provider.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { Organization } from "jslib-common/models/domain/organization";
import { Provider } from "jslib-common/models/domain/provider";
import { WebProviderService } from "../services/webProvider.service"; import { ValidationService } from 'jslib-angular/services/validation.service';
import { ProviderService } from '../services/provider.service';
import { Organization } from 'jslib-common/models/domain/organization';
import { Provider } from 'jslib-common/models/domain/provider';
import { PlanType } from 'jslib-common/enums/planType';
@Component({ @Component({
selector: "provider-add-organization", selector: 'provider-add-organization',
templateUrl: "add-organization.component.html", templateUrl: 'add-organization.component.html',
}) })
export class AddOrganizationComponent implements OnInit { export class AddOrganizationComponent implements OnInit {
@Input() providerId: string; @Input() providerId: string;
@Input() organizations: Organization[]; @Input() organizations: Organization[];
@Output() onAddedOrganization = new EventEmitter(); @Output() onAddedOrganization = new EventEmitter();
@@ -22,13 +35,10 @@ export class AddOrganizationComponent implements OnInit {
formPromise: Promise<any>; formPromise: Promise<any>;
loading = true; loading = true;
constructor( constructor(private userService: UserService, private providerService: ProviderService,
private providerService: ProviderService, private toasterService: ToasterService, private i18nService: I18nService,
private webProviderService: WebProviderService, private platformUtilsService: PlatformUtilsService, private validationService: ValidationService,
private i18nService: I18nService, private apiService: ApiService) { }
private platformUtilsService: PlatformUtilsService,
private validationService: ValidationService
) {}
async ngOnInit() { async ngOnInit() {
await this.load(); await this.load();
@@ -39,7 +49,7 @@ export class AddOrganizationComponent implements OnInit {
return; return;
} }
this.provider = await this.providerService.get(this.providerId); this.provider = await this.userService.getProvider(this.providerId);
this.loading = false; this.loading = false;
} }
@@ -50,22 +60,15 @@ export class AddOrganizationComponent implements OnInit {
} }
const confirmed = await this.platformUtilsService.showDialog( const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("addOrganizationConfirmation", organization.name, this.provider.name), this.i18nService.t('addOrganizationConfirmation', organization.name, this.provider.name), organization.name,
organization.name, this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) { if (!confirmed) {
return false; return false;
} }
try { try {
this.formPromise = this.webProviderService.addOrganizationToProvider( this.formPromise = this.providerService.addOrganizationToProvider(this.providerId, organization.id);
this.providerId,
organization.id
);
await this.formPromise; await this.formPromise;
} catch (e) { } catch (e) {
this.validationService.showError(e); this.validationService.showError(e);
@@ -74,11 +77,7 @@ export class AddOrganizationComponent implements OnInit {
this.formPromise = null; this.formPromise = null;
} }
this.platformUtilsService.showToast( this.toasterService.popAsync('success', null, this.i18nService.t('organizationJoinedProvider'));
"success",
null,
this.i18nService.t("organizationJoinedProvider")
);
this.onAddedOrganization.emit(); this.onAddedOrganization.emit();
} }
} }

View File

@@ -1,82 +1,54 @@
<div class="page-header d-flex"> <div class="page-header d-flex">
<h1>{{ "clients" | i18n }}</h1> <h1>{{'clients' | i18n}}</h1>
<div class="ml-auto d-flex"> <div class="ml-auto d-flex">
<div> <div>
<label class="sr-only" for="search">{{ "search" | i18n }}</label> <label class="sr-only" for="search">{{'search' | i18n}}</label>
<input <input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
type="search" [(ngModel)]="searchText">
class="form-control form-control-sm"
id="search"
placeholder="{{ 'search' | i18n }}"
[(ngModel)]="searchText"
/>
</div> </div>
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations"> <a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i> <i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{ "newClientOrganization" | i18n }} {{'newClientOrganization' | i18n}}
</a> </a>
<button <button class="btn btn-sm btn-outline-primary ml-3" (click)="addExistingOrganization()"
class="btn btn-sm btn-outline-primary ml-3" *ngIf="manageOrganizations && showAddExisting">
(click)="addExistingOrganization()" <i class="fa fa-plus fa-fw" aria-hidden="true"></i>
*ngIf="manageOrganizations && showAddExisting" {{'addExistingOrganization' | i18n}}
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "addExistingOrganization" | i18n }}
</button> </button>
</div> </div>
</div> </div>
<ng-container *ngIf="loading"> <ng-container *ngIf="loading">
<i <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
<ng-container <ng-container
*ngIf="!loading && (clients | search: searchText:'organizationName':'id') as searchedClients" *ngIf="!loading && (clients | search:searchText:'organizationName':'id') as searchedClients">
> <p *ngIf="!searchedClients.length">{{'noClientsInList' | i18n}}</p>
<p *ngIf="!searchedClients.length">{{ "noClientsInList" | i18n }}</p>
<ng-container *ngIf="searchedClients.length"> <ng-container *ngIf="searchedClients.length">
<table <table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
class="table table-hover table-list" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<tbody> <tbody>
<tr *ngFor="let o of searchedClients"> <tr *ngFor="let o of searchedClients">
<td width="30"> <td width="30">
<app-avatar <app-avatar [data]="o.organizationName" size="25" [circle]="true" [fontSize]="14"></app-avatar>
[data]="o.organizationName"
size="25"
[circle]="true"
[fontSize]="14"
></app-avatar>
</td> </td>
<td> <td>
<a [routerLink]="['/organizations', o.organizationId]">{{ o.organizationName }}</a> <a [routerLink]="['/organizations', o.organizationId]">{{o.organizationName}}</a>
</td> </td>
<td class="table-list-options" *ngIf="manageOrganizations"> <td class="table-list-options" *ngIf="manageOrganizations">
<div class="dropdown" appListDropdown> <div class="dropdown" appListDropdown>
<button <button class="btn btn-outline-secondary dropdown-toggle" type="button"
class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
type="button" appA11yTitle="{{'options' | i18n}}">
data-toggle="dropdown" <i class="fa fa-cog fa-lg" aria-hidden="true"></i>
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)"> <a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i> <i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{ "remove" | i18n }} {{'remove' | i18n}}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,36 +1,44 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import {
import { ActivatedRoute } from "@angular/router"; Component,
import { first } from "rxjs/operators"; OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { ModalService } from "jslib-angular/services/modal.service"; import { first } from 'rxjs/operators';
import { ValidationService } from "jslib-angular/services/validation.service";
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { PlanType } from "jslib-common/enums/planType";
import { ProviderUserType } from "jslib-common/enums/providerUserType";
import { Organization } from "jslib-common/models/domain/organization";
import { ProviderOrganizationOrganizationDetailsResponse } from "jslib-common/models/response/provider/providerOrganizationResponse";
import { WebProviderService } from "../services/webProvider.service"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SearchService } from 'jslib-common/abstractions/search.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { AddOrganizationComponent } from "./add-organization.component"; import { ModalService } from 'jslib-angular/services/modal.service';
import { ValidationService } from 'jslib-angular/services/validation.service';
const DisallowedPlanTypes = [ import { PlanType } from 'jslib-common/enums/planType';
PlanType.Free, import { ProviderUserType } from 'jslib-common/enums/providerUserType';
PlanType.FamiliesAnnually2019,
PlanType.FamiliesAnnually, import { Organization } from 'jslib-common/models/domain/organization';
]; import {
ProviderOrganizationOrganizationDetailsResponse
} from 'jslib-common/models/response/provider/providerOrganizationResponse';
import { ProviderService } from '../services/provider.service';
import { AddOrganizationComponent } from './add-organization.component';
const DisallowedPlanTypes = [PlanType.Free, PlanType.FamiliesAnnually2019, PlanType.FamiliesAnnually];
@Component({ @Component({
templateUrl: "clients.component.html", templateUrl: 'clients.component.html',
}) })
export class ClientsComponent implements OnInit { export class ClientsComponent implements OnInit {
@ViewChild("add", { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
@ViewChild('add', { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
providerId: any; providerId: any;
searchText: string; searchText: string;
@@ -47,27 +55,20 @@ export class ClientsComponent implements OnInit {
protected actionPromise: Promise<any>; protected actionPromise: Promise<any>;
private pagedClientsCount = 0; private pagedClientsCount = 0;
constructor( constructor(private route: ActivatedRoute, private userService: UserService,
private route: ActivatedRoute, private apiService: ApiService, private searchService: SearchService,
private providerService: ProviderService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private apiService: ApiService, private toasterService: ToasterService, private validationService: ValidationService,
private searchService: SearchService, private providerService: ProviderService, private logService: LogService,
private platformUtilsService: PlatformUtilsService, private modalService: ModalService) { }
private i18nService: I18nService,
private validationService: ValidationService,
private webProviderService: WebProviderService,
private logService: LogService,
private modalService: ModalService,
private organizationService: OrganizationService
) {}
async ngOnInit() { async ngOnInit() {
this.route.parent.params.subscribe(async (params) => { this.route.parent.params.subscribe(async params => {
this.providerId = params.providerId; this.providerId = params.providerId;
await this.load(); await this.load();
this.route.queryParams.pipe(first()).subscribe(async (qParams) => { this.route.queryParams.pipe(first()).subscribe(async qParams => {
this.searchText = qParams.search; this.searchText = qParams.search;
}); });
}); });
@@ -76,17 +77,12 @@ export class ClientsComponent implements OnInit {
async load() { async load() {
const response = await this.apiService.getProviderClients(this.providerId); const response = await this.apiService.getProviderClients(this.providerId);
this.clients = response.data != null && response.data.length > 0 ? response.data : []; this.clients = response.data != null && response.data.length > 0 ? response.data : [];
this.manageOrganizations = this.manageOrganizations = (await this.userService.getProvider(this.providerId)).type === ProviderUserType.ProviderAdmin;
(await this.providerService.get(this.providerId)).type === ProviderUserType.ProviderAdmin; const candidateOrgs = (await this.userService.getAllOrganizations()).filter(o => o.isOwner && o.providerId == null);
const candidateOrgs = (await this.organizationService.getAll()).filter( const allowedOrgsIds = await Promise.all(candidateOrgs.map(o => this.apiService.getOrganization(o.id))).then(orgs =>
(o) => o.isOwner && o.providerId == null orgs.filter(o => !DisallowedPlanTypes.includes(o.planType))
); .map(o => o.id));
const allowedOrgsIds = await Promise.all( this.addableOrganizations = candidateOrgs.filter(o => allowedOrgsIds.includes(o.id));
candidateOrgs.map((o) => this.apiService.getOrganization(o.id))
).then((orgs) =>
orgs.filter((o) => !DisallowedPlanTypes.includes(o.planType)).map((o) => o.id)
);
this.addableOrganizations = candidateOrgs.filter((o) => allowedOrgsIds.includes(o.id));
this.showAddExisting = this.addableOrganizations.length !== 0; this.showAddExisting = this.addableOrganizations.length !== 0;
this.loading = false; this.loading = false;
@@ -109,6 +105,7 @@ export class ClientsComponent implements OnInit {
this.loadMore(); this.loadMore();
} }
loadMore() { loadMore() {
if (!this.clients || this.clients.length <= this.pageSize) { if (!this.clients || this.clients.length <= this.pageSize) {
return; return;
@@ -119,19 +116,14 @@ export class ClientsComponent implements OnInit {
pagedSize = this.pagedClientsCount; pagedSize = this.pagedClientsCount;
} }
if (this.clients.length > pagedLength) { if (this.clients.length > pagedLength) {
this.pagedClients = this.pagedClients.concat( this.pagedClients = this.pagedClients.concat(this.clients.slice(pagedLength, pagedLength + pagedSize));
this.clients.slice(pagedLength, pagedLength + pagedSize)
);
} }
this.pagedClientsCount = this.pagedClients.length; this.pagedClientsCount = this.pagedClients.length;
this.didScroll = this.pagedClients.length > this.pageSize; this.didScroll = this.pagedClients.length > this.pageSize;
} }
async addExistingOrganization() { async addExistingOrganization() {
const [modal] = await this.modalService.openViewRef( const [modal] = await this.modalService.openViewRef(AddOrganizationComponent, this.addModalRef, comp => {
AddOrganizationComponent,
this.addModalRef,
(comp) => {
comp.providerId = this.providerId; comp.providerId = this.providerId;
comp.organizations = this.addableOrganizations; comp.organizations = this.addableOrganizations;
comp.onAddedOrganization.subscribe(async () => { comp.onAddedOrganization.subscribe(async () => {
@@ -142,34 +134,22 @@ export class ClientsComponent implements OnInit {
this.logService.error(`Handled exception: ${e}`); this.logService.error(`Handled exception: ${e}`);
} }
}); });
} });
);
} }
async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { async remove(organization: ProviderOrganizationOrganizationDetailsResponse) {
const confirmed = await this.platformUtilsService.showDialog( const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("detachOrganizationConfirmation"), this.i18nService.t('detachOrganizationConfirmation'), organization.organizationName,
organization.organizationName, this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) { if (!confirmed) {
return false; return false;
} }
this.actionPromise = this.webProviderService.detachOrganizastion( this.actionPromise = this.providerService.detachOrganizastion(this.providerId, organization.id);
this.providerId,
organization.id
);
try { try {
await this.actionPromise; await this.actionPromise;
this.platformUtilsService.showToast( this.toasterService.popAsync('success', null, this.i18nService.t('detachedOrganization', organization.organizationName));
"success",
null,
this.i18nService.t("detachedOrganization", organization.organizationName)
);
await this.load(); await this.load();
} catch (e) { } catch (e) {
this.validationService.showError(e); this.validationService.showError(e);

View File

@@ -1,5 +1,5 @@
<div class="page-header"> <div class="page-header">
<h1>{{ "newClientOrganization" | i18n }}</h1> <h1>{{'newClientOrganization' | i18n}}</h1>
</div> </div>
<p>{{ "newClientOrganizationDesc" | i18n }}</p> <p>{{'newClientOrganizationDesc' | i18n}}</p>
<app-organization-plans [providerId]="providerId"></app-organization-plans> <app-organization-plans [providerId]="providerId"></app-organization-plans>

View File

@@ -1,22 +1,25 @@
import { Component, OnInit, ViewChild } from "@angular/core"; import {
import { ActivatedRoute } from "@angular/router"; Component,
OnInit,
ViewChild,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { OrganizationPlansComponent } from "src/app/settings/organization-plans.component"; import { OrganizationPlansComponent } from 'src/app/settings/organization-plans.component';
@Component({ @Component({
selector: "app-create-organization", selector: 'app-create-organization',
templateUrl: "create-organization.component.html", templateUrl: 'create-organization.component.html',
}) })
export class CreateOrganizationComponent implements OnInit { export class CreateOrganizationComponent implements OnInit {
@ViewChild(OrganizationPlansComponent, { static: true }) @ViewChild(OrganizationPlansComponent, { static: true }) orgPlansComponent: OrganizationPlansComponent;
orgPlansComponent: OrganizationPlansComponent;
providerId: string; providerId: string;
constructor(private route: ActivatedRoute) {} constructor(private route: ActivatedRoute) { }
ngOnInit() { ngOnInit() {
this.route.parent.params.subscribe(async (params) => { this.route.parent.params.subscribe(async params => {
this.providerId = params.providerId; this.providerId = params.providerId;
}); });
} }

View File

@@ -1,26 +0,0 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { Permissions } from "jslib-common/enums/permissions";
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private providerService: ProviderService, private router: Router) {}
async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.providerService.get(route.params.providerId);
const permissions = route.data == null ? null : (route.data.permissions as Permissions[]);
if (
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) ||
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) ||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers)
) {
return true;
}
this.router.navigate(["/providers", provider.id]);
return false;
}
}

View File

@@ -1,31 +0,0 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
@Injectable()
export class ProviderGuard implements CanActivate {
constructor(
private router: Router,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private providerService: ProviderService
) {}
async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.providerService.get(route.params.providerId);
if (provider == null) {
this.router.navigate(["/"]);
return false;
}
if (!provider.isProviderAdmin && !provider.enabled) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("providerIsDisabled"));
this.router.navigate(["/"]);
return false;
}
return true;
}
}

View File

@@ -1,42 +1,31 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading"> <div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div> <div>
<img class="mb-4 logo logo-themed" alt="Bitwarden" /> <img class="mb-4 logo logo-themed" alt="Bitwarden">
<p class="text-center"> <p class="text-center">
<i <i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin bwi-2x text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p> </p>
</div> </div>
</div> </div>
<div class="container" *ngIf="!loading && !authed"> <div class="container" *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="lead text-center mb-4">{{ "joinProvider" | i18n }}</p> <p class="lead text-center mb-4">{{'joinProvider' | i18n}}</p>
<div class="card d-block"> <div class="card d-block">
<div class="card-body"> <div class="card-body">
<p class="text-center"> <p class="text-center">
{{ providerName }} {{providerName}}
<strong class="d-block mt-2">{{ email }}</strong> <strong class="d-block mt-2">{{email}}</strong>
</p> </p>
<p>{{ "joinProviderDesc" | i18n }}</p> <p>{{'joinProviderDesc' | i18n}}</p>
<hr /> <hr>
<div class="d-flex"> <div class="d-flex">
<a <a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
routerLink="/login" {{'logIn' | i18n}}
[queryParams]="{ email: email }"
class="btn btn-primary btn-block"
>
{{ "logIn" | i18n }}
</a> </a>
<a <a routerLink="/register" [queryParams]="{email: email}"
routerLink="/register" class="btn btn-primary btn-block ml-2 mt-0">
[queryParams]="{ email: email }" {{'createAccount' | i18n}}
class="btn btn-primary btn-block ml-2 mt-0"
>
{{ "createAccount" | i18n }}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,52 +1,45 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from '@angular/router';
import { Toast, ToasterService } from 'angular2-toaster';
import { ApiService } from "jslib-common/abstractions/api.service"; import { BaseAcceptComponent } from 'src/app/common/base.accept.component';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { ProviderUserAcceptRequest } from "jslib-common/models/request/provider/providerUserAcceptRequest";
import { BaseAcceptComponent } from "src/app/common/base.accept.component"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ProviderUserAcceptRequest } from 'jslib-common/models/request/provider/providerUserAcceptRequest';
@Component({ @Component({
selector: "app-accept-provider", selector: 'app-accept-provider',
templateUrl: "accept-provider.component.html", templateUrl: 'accept-provider.component.html',
}) })
export class AcceptProviderComponent extends BaseAcceptComponent { export class AcceptProviderComponent extends BaseAcceptComponent {
providerName: string; providerName: string;
failedMessage = "providerInviteAcceptFailed"; failedMessage = 'providerInviteAcceptFailed';
requiredParameters = ["providerId", "providerUserId", "token"]; requiredParameters = ['providerId', 'providerUserId', 'token'];
constructor( constructor(router: Router, toasterService: ToasterService, i18nService: I18nService, route: ActivatedRoute,
router: Router, userService: UserService, stateService: StateService, private apiService: ApiService) {
i18nService: I18nService, super(router, toasterService, i18nService, route, userService, stateService);
route: ActivatedRoute,
stateService: StateService,
private apiService: ApiService,
platformUtilService: PlatformUtilsService
) {
super(router, platformUtilService, i18nService, route, stateService);
} }
async authedHandler(qParams: any) { async authedHandler(qParams: any) {
const request = new ProviderUserAcceptRequest(); const request = new ProviderUserAcceptRequest();
request.token = qParams.token; request.token = qParams.token;
await this.apiService.postProviderUserAccept( await this.apiService.postProviderUserAccept(qParams.providerId, qParams.providerUserId, request);
qParams.providerId, const toast: Toast = {
qParams.providerUserId, type: 'success',
request title: this.i18nService.t('inviteAccepted'),
); body: this.i18nService.t('providerInviteAcceptedDesc'),
this.platformUtilService.showToast( timeout: 10000,
"success", };
this.i18nService.t("inviteAccepted"), this.toasterService.popAsync(toast);
this.i18nService.t("providerInviteAcceptedDesc"), this.router.navigate(['/vault']);
{ timeout: 10000 }
);
this.router.navigate(["/vault"]);
} }
async unauthedHandler(qParams: any) { async unauthedHandler(qParams: any) {

View File

@@ -1,16 +1,21 @@
import { Component, Input } from "@angular/core"; import {
Component,
Input,
} from '@angular/core';
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType"; import { ProviderUserBulkConfirmRequest } from 'jslib-common/models/request/provider/providerUserBulkConfirmRequest';
import { ProviderUserBulkConfirmRequest } from "jslib-common/models/request/provider/providerUserBulkConfirmRequest"; import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "src/app/organizations/manage/bulk/bulk-confirm.component"; import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
import { BulkUserDetails } from "src/app/organizations/manage/bulk/bulk-status.component";
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from 'src/app/organizations/manage/bulk/bulk-confirm.component';
import { BulkUserDetails } from 'src/app/organizations/manage/bulk/bulk-status.component';
@Component({ @Component({
templateUrl: "../../../../../../src/app/organizations/manage/bulk/bulk-confirm.component.html", templateUrl: '/src/app/organizations/manage/bulk/bulk-confirm.component.html',
}) })
export class BulkConfirmComponent extends OrganizationBulkConfirmComponent { export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
@Input() providerId: string; @Input() providerId: string;
protected isAccepted(user: BulkUserDetails) { protected isAccepted(user: BulkUserDetails) {
@@ -18,7 +23,7 @@ export class BulkConfirmComponent extends OrganizationBulkConfirmComponent {
} }
protected async getPublicKeys() { protected async getPublicKeys() {
const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id)); const request = new ProviderUserBulkRequest(this.filteredUsers.map(user => user.id));
return await this.apiService.postProviderUsersPublicKey(this.providerId, request); return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
} }

View File

@@ -1,17 +1,21 @@
import { Component, Input } from "@angular/core"; import {
Component,
Input,
} from '@angular/core';
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest"; import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from "src/app/organizations/manage/bulk/bulk-remove.component"; import { BulkRemoveComponent as OrganizationBulkRemoveComponent } from 'src/app/organizations/manage/bulk/bulk-remove.component';
@Component({ @Component({
templateUrl: "../../../../../../src/app/organizations/manage/bulk/bulk-remove.component.html", templateUrl: '/src/app/organizations/manage/bulk/bulk-remove.component.html',
}) })
export class BulkRemoveComponent extends OrganizationBulkRemoveComponent { export class BulkRemoveComponent extends OrganizationBulkRemoveComponent {
@Input() providerId: string; @Input() providerId: string;
async deleteUsers() { async deleteUsers() {
const request = new ProviderUserBulkRequest(this.users.map((user) => user.id)); const request = new ProviderUserBulkRequest(this.users.map(user => user.id));
return await this.apiService.deleteManyProviderUsers(this.providerId, request); return await this.apiService.deleteManyProviderUsers(this.providerId, request);
} }
} }

View File

@@ -1,107 +1,68 @@
<div class="page-header d-flex"> <div class="page-header d-flex">
<h1>{{ "eventLogs" | i18n }}</h1> <h1>{{'eventLogs' | i18n}}</h1>
<div class="ml-auto d-flex"> <div class="ml-auto d-flex">
<div class="form-inline"> <div class="form-inline">
<label class="sr-only" for="start">{{ "startDate" | i18n }}</label> <label class="sr-only" for="start">{{'startDate' | i18n}}</label>
<input <input type="datetime-local" class="form-control form-control-sm" id="start"
type="datetime-local" placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM"
class="form-control form-control-sm" (change)="dirtyDates = true">
id="start"
placeholder="{{ 'startDate' | i18n }}"
[(ngModel)]="start"
placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true"
/>
<span class="mx-2">-</span> <span class="mx-2">-</span>
<label class="sr-only" for="end">{{ "endDate" | i18n }}</label> <label class="sr-only" for="end">{{'endDate' | i18n}}</label>
<input <input type="datetime-local" class="form-control form-control-sm" id="end"
type="datetime-local" placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM"
class="form-control form-control-sm" (change)="dirtyDates = true">
id="end"
placeholder="{{ 'endDate' | i18n }}"
[(ngModel)]="end"
placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true"
/>
</div> </div>
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline"> <form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
<button <button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
type="button" [disabled]="loaded && refreshForm.loading">
class="btn btn-sm btn-outline-primary ml-3" <i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshForm.loading}"></i>
(click)="loadEvents(true)" {{'refresh' | i18n}}
[disabled]="loaded && refreshForm.loading"
>
<i
class="bwi bwi-refresh bwi-fw"
aria-hidden="true"
[ngClass]="{ 'bwi-spin': loaded && refreshForm.loading }"
></i>
{{ "refresh" | i18n }}
</button> </button>
</form> </form>
<form #exportForm [appApiAction]="exportPromise" class="d-inline"> <form #exportForm [appApiAction]="exportPromise" class="d-inline">
<button <button type="button" class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
type="button" [ngClass]="{loading:exportForm.loading}" (click)="exportEvents()"
class="btn btn-sm btn-outline-primary btn-submit manual ml-3" [disabled]="loaded && exportForm.loading || dirtyDates">
[ngClass]="{ loading: exportForm.loading }" <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
(click)="exportEvents()" <span>{{'export' | i18n}}</span>
[disabled]="(loaded && exportForm.loading) || dirtyDates"
>
<i class="bwi bwi-spinner bwi-spin" aria-hidden="true"></i>
<span>{{ "export" | i18n }}</span>
</button> </button>
</form> </form>
</div> </div>
</div> </div>
<ng-container *ngIf="!loaded"> <ng-container *ngIf="!loaded">
<i <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
<ng-container *ngIf="loaded"> <ng-container *ngIf="loaded">
<p *ngIf="!events || !events.length">{{ "noEventsInList" | i18n }}</p> <p *ngIf="!events || !events.length">{{'noEventsInList' | i18n}}</p>
<table class="table table-hover" *ngIf="events && events.length"> <table class="table table-hover" *ngIf="events && events.length">
<thead> <thead>
<tr> <tr>
<th class="border-top-0" width="210">{{ "timestamp" | i18n }}</th> <th class="border-top-0" width="210">{{'timestamp' | i18n}}</th>
<th class="border-top-0" width="40"> <th class="border-top-0" width="40">
<span class="sr-only">{{ "device" | i18n }}</span> <span class="sr-only">{{'device' | i18n}}</span>
</th> </th>
<th class="border-top-0" width="150">{{ "user" | i18n }}</th> <th class="border-top-0" width="150">{{'user' | i18n}}</th>
<th class="border-top-0">{{ "event" | i18n }}</th> <th class="border-top-0">{{'event' | i18n}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let e of events"> <tr *ngFor="let e of events">
<td>{{ e.date | date: "medium" }}</td> <td>{{e.date | date:'medium'}}</td>
<td> <td>
<i <i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}" aria-hidden="true"></i>
class="text-muted bwi bwi-lg {{ e.appIcon }}" <span class="sr-only">{{e.appName}}, {{e.ip}}</span>
title="{{ e.appName }}, {{ e.ip }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ e.appName }}, {{ e.ip }}</span>
</td> </td>
<td> <td>
<span title="{{ e.userEmail }}">{{ e.userName }}</span> <span title="{{e.userEmail}}">{{e.userName}}</span>
</td> </td>
<td [innerHTML]="e.message"></td> <td [innerHTML]="e.message"></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<button <button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
#moreBtn (click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
[appApiAction]="morePromise" <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
type="button" <span>{{'loadMore' | i18n}}</span>
class="btn btn-block btn-link btn-submit"
(click)="loadEvents(false)"
[disabled]="loaded && moreBtn.loading"
*ngIf="continuationToken"
>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "loadMore" | i18n }}</span>
</button> </button>
</ng-container> </ng-container>

View File

@@ -1,50 +1,49 @@
import { Component, OnInit } from "@angular/core"; import {
import { ActivatedRoute, Router } from "@angular/router"; Component,
OnInit,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { ApiService } from "jslib-common/abstractions/api.service"; import { ExportService } from 'jslib-common/abstractions/export.service';
import { ExportService } from "jslib-common/abstractions/export.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { LogService } from 'jslib-common/abstractions/log.service';
import { LogService } from "jslib-common/abstractions/log.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { EventResponse } from "jslib-common/models/response/eventResponse";
import { BaseEventsComponent } from "src/app/common/base.events.component"; import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
import { EventService } from "src/app/services/event.service";
import { EventResponse } from 'jslib-common/models/response/eventResponse';
import { EventService } from 'src/app/services/event.service';
import { BaseEventsComponent } from 'src/app/common/base.events.component';
@Component({ @Component({
selector: "provider-events", selector: 'provider-events',
templateUrl: "events.component.html", templateUrl: 'events.component.html',
}) })
export class EventsComponent extends BaseEventsComponent implements OnInit { export class EventsComponent extends BaseEventsComponent implements OnInit {
exportFileName = "provider-events"; exportFileName: string = 'provider-events';
providerId: string; providerId: string;
private providerUsersUserIdMap = new Map<string, any>(); private providerUsersUserIdMap = new Map<string, any>();
private providerUsersIdMap = new Map<string, any>(); private providerUsersIdMap = new Map<string, any>();
constructor( constructor(private apiService: ApiService, private route: ActivatedRoute, eventService: EventService,
private apiService: ApiService, i18nService: I18nService, toasterService: ToasterService, private userService: UserService,
private route: ActivatedRoute, exportService: ExportService, platformUtilsService: PlatformUtilsService, private router: Router,
eventService: EventService, logService: LogService, private userNamePipe: UserNamePipe) {
i18nService: I18nService, super(eventService, i18nService, toasterService, exportService, platformUtilsService, logService);
private providerService: ProviderService,
exportService: ExportService,
platformUtilsService: PlatformUtilsService,
private router: Router,
logService: LogService,
private userNamePipe: UserNamePipe
) {
super(eventService, i18nService, exportService, platformUtilsService, logService);
} }
async ngOnInit() { async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async params => {
this.providerId = params.providerId; this.providerId = params.providerId;
const provider = await this.providerService.get(this.providerId); const provider = await this.userService.getProvider(this.providerId);
if (provider == null || !provider.useEvents) { if (provider == null || !provider.useEvents) {
this.router.navigate(["/providers", this.providerId]); this.router.navigate(['/providers', this.providerId]);
return; return;
} }
await this.load(); await this.load();
@@ -53,7 +52,7 @@ export class EventsComponent extends BaseEventsComponent implements OnInit {
async load() { async load() {
const response = await this.apiService.getProviderUsers(this.providerId); const response = await this.apiService.getProviderUsers(this.providerId);
response.data.forEach((u) => { response.data.forEach(u => {
const name = this.userNamePipe.transform(u); const name = this.userNamePipe.transform(u);
this.providerUsersIdMap.set(u.id, { name: name, email: u.email }); this.providerUsersIdMap.set(u.id, { name: name, email: u.email });
this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email }); this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email });
@@ -63,17 +62,10 @@ export class EventsComponent extends BaseEventsComponent implements OnInit {
} }
protected requestEvents(startDate: string, endDate: string, continuationToken: string) { protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
return this.apiService.getEventsProvider( return this.apiService.getEventsProvider(this.providerId, startDate, endDate, continuationToken);
this.providerId,
startDate,
endDate,
continuationToken
);
} }
protected getUserName(r: EventResponse, userId: string) { protected getUserName(r: EventResponse, userId: string) {
return userId != null && this.providerUsersUserIdMap.has(userId) return userId != null && this.providerUsersUserIdMap.has(userId) ? this.providerUsersUserIdMap.get(userId) : null;
? this.providerUsersUserIdMap.get(userId)
: null;
} }
} }

View File

@@ -2,23 +2,15 @@
<div class="row"> <div class="row">
<div class="col-3"> <div class="col-3">
<div class="card" *ngIf="provider"> <div class="card" *ngIf="provider">
<div class="card-header">{{ "manage" | i18n }}</div> <div class="card-header">{{'manage' | i18n}}</div>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<a <a routerLink="people" class="list-group-item" routerLinkActive="active"
routerLink="people" *ngIf="provider.canManageUsers">
class="list-group-item" {{'people' | i18n}}
routerLinkActive="active"
*ngIf="provider.canManageUsers"
>
{{ "people" | i18n }}
</a> </a>
<a <a routerLink="events" class="list-group-item" routerLinkActive="active"
routerLink="events" *ngIf="provider.canAccessEventLogs && accessEvents">
class="list-group-item" {{'eventLogs' | i18n}}
routerLinkActive="active"
*ngIf="provider.canAccessEventLogs && accessEvents"
>
{{ "eventLogs" | i18n }}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,22 +1,26 @@
import { Component, OnInit } from "@angular/core"; import {
import { ActivatedRoute } from "@angular/router"; Component,
OnInit,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProviderService } from "jslib-common/abstractions/provider.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { Provider } from "jslib-common/models/domain/provider";
import { Provider } from 'jslib-common/models/domain/provider';
@Component({ @Component({
selector: "provider-manage", selector: 'provider-manage',
templateUrl: "manage.component.html", templateUrl: 'manage.component.html',
}) })
export class ManageComponent implements OnInit { export class ManageComponent implements OnInit {
provider: Provider; provider: Provider;
accessEvents = false; accessEvents = false;
constructor(private route: ActivatedRoute, private providerService: ProviderService) {} constructor(private route: ActivatedRoute, private userService: UserService) { }
ngOnInit() { ngOnInit() {
this.route.parent.params.subscribe(async (params) => { this.route.parent.params.subscribe(async params => {
this.provider = await this.providerService.get(params.providerId); this.provider = await this.userService.getProvider(params.providerId);
this.accessEvents = this.provider.useEvents; this.accessEvents = this.provider.useEvents;
}); });
} }

View File

@@ -1,221 +1,137 @@
<div class="page-header d-flex"> <div class="page-header d-flex">
<h1>{{ "people" | i18n }}</h1> <h1>{{'people' | i18n}}</h1>
<div class="ml-auto d-flex"> <div class="ml-auto d-flex">
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
<button <button type="button" class="btn btn-outline-secondary" [ngClass]="{active: status == null}"
type="button" (click)="filter(null)">
class="btn btn-outline-secondary" {{'all' | i18n}}
[ngClass]="{ active: status == null }" <span class="badge badge-pill badge-info" *ngIf="allCount">{{allCount}}</span>
(click)="filter(null)"
>
{{ "all" | i18n }}
<span class="badge badge-pill badge-info" *ngIf="allCount">{{ allCount }}</span>
</button> </button>
<button <button type="button" class="btn btn-outline-secondary"
type="button" [ngClass]="{active: status == userStatusType.Invited}"
class="btn btn-outline-secondary" (click)="filter(userStatusType.Invited)">
[ngClass]="{ active: status == userStatusType.Invited }" {{'invited' | i18n}}
(click)="filter(userStatusType.Invited)" <span class="badge badge-pill badge-info" *ngIf="invitedCount">{{invitedCount}}</span>
>
{{ "invited" | i18n }}
<span class="badge badge-pill badge-info" *ngIf="invitedCount">{{ invitedCount }}</span>
</button> </button>
<button <button type="button" class="btn btn-outline-secondary"
type="button" [ngClass]="{active: status == userStatusType.Accepted}"
class="btn btn-outline-secondary" (click)="filter(userStatusType.Accepted)">
[ngClass]="{ active: status == userStatusType.Accepted }" {{'accepted' | i18n}}
(click)="filter(userStatusType.Accepted)" <span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{acceptedCount}}</span>
>
{{ "accepted" | i18n }}
<span class="badge badge-pill badge-warning" *ngIf="acceptedCount">{{
acceptedCount
}}</span>
</button> </button>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<label class="sr-only" for="search">{{ "search" | i18n }}</label> <label class="sr-only" for="search">{{'search' | i18n}}</label>
<input <input type="search" class="form-control form-control-sm" id="search" placeholder="{{'search' | i18n}}"
type="search" [(ngModel)]="searchText">
class="form-control form-control-sm"
id="search"
placeholder="{{ 'search' | i18n }}"
[(ngModel)]="searchText"
/>
</div> </div>
<div class="dropdown ml-3" appListDropdown> <div class="dropdown ml-3" appListDropdown>
<button <button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="bulkActionsButton"
class="btn btn-sm btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
type="button" <i class="fa fa-cog" aria-hidden="true"></i>
id="bulkActionsButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton"> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="bulkActionsButton">
<button class="dropdown-item" appStopClick (click)="bulkReinvite()"> <button class="dropdown-item" appStopClick (click)="bulkReinvite()">
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i> <i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
{{ "reinviteSelected" | i18n }} {{'reinviteSelected' | i18n}}
</button> </button>
<button <button class="dropdown-item text-success" appStopClick (click)="bulkConfirm()"
class="dropdown-item text-success" *ngIf="showBulkConfirmUsers">
appStopClick <i class="fa fa-fw fa-check" aria-hidden="true"></i>
(click)="bulkConfirm()" {{'confirmSelected' | i18n}}
*ngIf="showBulkConfirmUsers"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirmSelected" | i18n }}
</button> </button>
<button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()"> <button class="dropdown-item text-danger" appStopClick (click)="bulkRemove()">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i> <i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{ "remove" | i18n }} {{'remove' | i18n}}
</button> </button>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<button class="dropdown-item" appStopClick (click)="selectAll(true)"> <button class="dropdown-item" appStopClick (click)="selectAll(true)">
<i class="bwi bwi-fw bwi-check-square" aria-hidden="true"></i> <i class="fa fa-fw fa-check-square-o" aria-hidden="true"></i>
{{ "selectAll" | i18n }} {{'selectAll' | i18n}}
</button> </button>
<button class="dropdown-item" appStopClick (click)="selectAll(false)"> <button class="dropdown-item" appStopClick (click)="selectAll(false)">
<i class="bwi bwi-fw bwi-minus-square" aria-hidden="true"></i> <i class="fa fa-fw fa-minus-square-o" aria-hidden="true"></i>
{{ "unselectAll" | i18n }} {{'unselectAll' | i18n}}
</button> </button>
</div> </div>
</div> </div>
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()"> <button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="invite()">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i> <i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{ "inviteUser" | i18n }} {{'inviteUser' | i18n}}
</button> </button>
</div> </div>
</div> </div>
<ng-container *ngIf="loading"> <ng-container *ngIf="loading">
<i <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
<ng-container <ng-container
*ngIf=" *ngIf="!loading && (isPaging() ? pagedUsers : users | search:searchText:'name':'email':'id') as searchedUsers">
!loading && <p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
(isPaging() ? pagedUsers : (users | search: searchText:'name':'email':'id')) as searchedUsers
"
>
<p *ngIf="!searchedUsers.length">{{ "noUsersInList" | i18n }}</p>
<ng-container *ngIf="searchedUsers.length"> <ng-container *ngIf="searchedUsers.length">
<app-callout <app-callout type="info" title="{{'confirmUsers' | i18n}}" icon="fa-check-circle" *ngIf="showConfirmUsers">
type="info" {{'providerUsersNeedConfirmed' | i18n}}
title="{{ 'confirmUsers' | i18n }}"
icon="bwi bwi-check-circle"
*ngIf="showConfirmUsers"
>
{{ "providerUsersNeedConfirmed" | i18n }}
</app-callout> </app-callout>
<table <table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
class="table table-hover table-list" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<tbody> <tbody>
<tr *ngFor="let u of searchedUsers"> <tr *ngFor="let u of searchedUsers">
<td (click)="checkUser(u)" class="table-list-checkbox"> <td (click)="checkUser(u)" class="table-list-checkbox">
<input type="checkbox" [(ngModel)]="u.checked" appStopProp /> <input type="checkbox" [(ngModel)]="u.checked" appStopProp>
</td> </td>
<td width="30"> <td width="30">
<app-avatar <app-avatar [data]="u | userName" [email]="u.email" size="25" [circle]="true"
[data]="u | userName" [fontSize]="14"></app-avatar>
[email]="u.email"
size="25"
[circle]="true"
[fontSize]="14"
>
</app-avatar>
</td> </td>
<td> <td>
<a href="#" appStopClick (click)="edit(u)">{{ u.email }}</a> <a href="#" appStopClick (click)="edit(u)">{{u.email}}</a>
<span class="badge badge-secondary" *ngIf="u.status === userStatusType.Invited">{{ <span class="badge badge-secondary"
"invited" | i18n *ngIf="u.status === userStatusType.Invited">{{'invited' | i18n}}</span>
}}</span> <span class="badge badge-warning"
<span class="badge badge-warning" *ngIf="u.status === userStatusType.Accepted">{{ *ngIf="u.status === userStatusType.Accepted">{{'accepted' | i18n}}</span>
"accepted" | i18n <small class="text-muted d-block" *ngIf="u.name">{{u.name}}</small>
}}</span>
<small class="text-muted d-block" *ngIf="u.name">{{ u.name }}</small>
</td> </td>
<td> <td>
<ng-container *ngIf="u.twoFactorEnabled"> <ng-container *ngIf="u.twoFactorEnabled">
<i <i class="fa fa-lock" title="{{'userUsingTwoStep' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-lock" <span class="sr-only">{{'userUsingTwoStep' | i18n}}</span>
title="{{ 'userUsingTwoStep' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "userUsingTwoStep" | i18n }}</span>
</ng-container> </ng-container>
</td> </td>
<td> <td>
<span *ngIf="u.type === userType.ProviderAdmin">{{ "providerAdmin" | i18n }}</span> <span *ngIf="u.type === userType.ProviderAdmin">{{'providerAdmin' | i18n}}</span>
<span *ngIf="u.type === userType.ServiceUser">{{ "serviceUser" | i18n }}</span> <span *ngIf="u.type === userType.ServiceUser">{{'serviceUser' | i18n}}</span>
<span *ngIf="u.type === userType.Custom">{{ "custom" | i18n }}</span> <span *ngIf="u.type === userType.Custom">{{'custom' | i18n}}</span>
</td> </td>
<td class="table-list-options"> <td class="table-list-options">
<div class="dropdown" appListDropdown> <div class="dropdown" appListDropdown>
<button <button class="btn btn-outline-secondary dropdown-toggle" type="button"
class="btn btn-outline-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
type="button" appA11yTitle="{{'options' | i18n}}">
data-toggle="dropdown" <i class="fa fa-cog fa-lg" aria-hidden="true"></i>
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
</button> </button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a <a class="dropdown-item" href="#" appStopClick (click)="reinvite(u)"
class="dropdown-item" *ngIf="u.status === userStatusType.Invited">
href="#" <i class="fa fa-fw fa-envelope-o" aria-hidden="true"></i>
appStopClick {{'resendInvitation' | i18n}}
(click)="reinvite(u)"
*ngIf="u.status === userStatusType.Invited"
>
<i class="bwi bwi-fw bwi-envelope" aria-hidden="true"></i>
{{ "resendInvitation" | i18n }}
</a> </a>
<a <a class="dropdown-item text-success" href="#" appStopClick (click)="confirm(u)"
class="dropdown-item text-success" *ngIf="u.status === userStatusType.Accepted">
href="#" <i class="fa fa-fw fa-check" aria-hidden="true"></i>
appStopClick {{'confirm' | i18n}}
(click)="confirm(u)"
*ngIf="u.status === userStatusType.Accepted"
>
<i class="bwi bwi-fw bwi-check" aria-hidden="true"></i>
{{ "confirm" | i18n }}
</a> </a>
<a <a class="dropdown-item" href="#" appStopClick (click)="groups(u)" *ngIf="accessGroups">
class="dropdown-item" <i class="fa fa-fw fa-sitemap" aria-hidden="true"></i>
href="#" {{'groups' | i18n}}
appStopClick
(click)="groups(u)"
*ngIf="accessGroups"
>
<i class="bwi bwi-fw bwi-sitemap" aria-hidden="true"></i>
{{ "groups" | i18n }}
</a> </a>
<a <a class="dropdown-item" href="#" appStopClick (click)="events(u)"
class="dropdown-item" *ngIf="accessEvents && u.status === userStatusType.Confirmed">
href="#" <i class="fa fa-fw fa-file-text-o" aria-hidden="true"></i>
appStopClick {{'eventLogs' | i18n}}
(click)="events(u)"
*ngIf="accessEvents && u.status === userStatusType.Confirmed"
>
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>
{{ "eventLogs" | i18n }}
</a> </a>
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)"> <a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(u)">
<i class="bwi bwi-fw bwi-close" aria-hidden="true"></i> <i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{ "remove" | i18n }} {{'remove' | i18n}}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,98 +1,81 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import {
import { ActivatedRoute, Router } from "@angular/router"; Component,
import { first } from "rxjs/operators"; OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { SearchPipe } from "jslib-angular/pipes/search.pipe"; import { first } from 'rxjs/operators';
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
import { ModalService } from "jslib-angular/services/modal.service";
import { ValidationService } from "jslib-angular/services/validation.service";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType";
import { ProviderUserType } from "jslib-common/enums/providerUserType";
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
import { ProviderUserConfirmRequest } from "jslib-common/models/request/provider/providerUserConfirmRequest";
import { ListResponse } from "jslib-common/models/response/listResponse";
import { ProviderUserBulkResponse } from "jslib-common/models/response/provider/providerUserBulkResponse";
import { ProviderUserUserDetailsResponse } from "jslib-common/models/response/provider/providerUserResponse";
import { BasePeopleComponent } from "src/app/common/base.people.component"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { BulkStatusComponent } from "src/app/organizations/manage/bulk/bulk-status.component"; import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { EntityEventsComponent } from "src/app/organizations/manage/entity-events.component"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SearchService } from 'jslib-common/abstractions/search.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component"; import { ModalService } from 'jslib-angular/services/modal.service';
import { BulkRemoveComponent } from "./bulk/bulk-remove.component"; import { ValidationService } from 'jslib-angular/services/validation.service';
import { UserAddEditComponent } from "./user-add-edit.component";
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { SearchPipe } from 'jslib-angular/pipes/search.pipe';
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
import { ListResponse } from 'jslib-common/models/response/listResponse';
import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse';
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { ProviderUserConfirmRequest } from 'jslib-common/models/request/provider/providerUserConfirmRequest';
import { ProviderUserBulkResponse } from 'jslib-common/models/response/provider/providerUserBulkResponse';
import { BasePeopleComponent } from 'src/app/common/base.people.component';
import { BulkStatusComponent } from 'src/app/organizations/manage/bulk/bulk-status.component';
import { EntityEventsComponent } from 'src/app/organizations/manage/entity-events.component';
import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
import { BulkRemoveComponent } from './bulk/bulk-remove.component';
import { UserAddEditComponent } from './user-add-edit.component';
@Component({ @Component({
selector: "provider-people", selector: 'provider-people',
templateUrl: "people.component.html", templateUrl: 'people.component.html',
}) })
export class PeopleComponent export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> implements OnInit {
extends BasePeopleComponent<ProviderUserUserDetailsResponse>
implements OnInit @ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
{ @ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef; @ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
@ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) @ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef;
groupsModalRef: ViewContainerRef; @ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef;
@ViewChild("eventsTemplate", { read: ViewContainerRef, static: true }) @ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef;
eventsModalRef: ViewContainerRef;
@ViewChild("bulkStatusTemplate", { read: ViewContainerRef, static: true })
bulkStatusModalRef: ViewContainerRef;
@ViewChild("bulkConfirmTemplate", { read: ViewContainerRef, static: true })
bulkConfirmModalRef: ViewContainerRef;
@ViewChild("bulkRemoveTemplate", { read: ViewContainerRef, static: true })
bulkRemoveModalRef: ViewContainerRef;
userType = ProviderUserType; userType = ProviderUserType;
userStatusType = ProviderUserStatusType; userStatusType = ProviderUserStatusType;
providerId: string; providerId: string;
accessEvents = false; accessEvents = false;
constructor( constructor(apiService: ApiService, private route: ActivatedRoute,
apiService: ApiService, i18nService: I18nService, modalService: ModalService,
private route: ActivatedRoute, platformUtilsService: PlatformUtilsService, toasterService: ToasterService,
i18nService: I18nService, cryptoService: CryptoService, private userService: UserService, private router: Router,
modalService: ModalService, storageService: StorageService, searchService: SearchService, validationService: ValidationService,
platformUtilsService: PlatformUtilsService, logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe) {
cryptoService: CryptoService, super(apiService, searchService, i18nService, platformUtilsService, toasterService, cryptoService,
private router: Router, storageService, validationService, modalService, logService, searchPipe, userNamePipe);
searchService: SearchService,
validationService: ValidationService,
logService: LogService,
searchPipe: SearchPipe,
userNamePipe: UserNamePipe,
stateService: StateService,
private providerService: ProviderService
) {
super(
apiService,
searchService,
i18nService,
platformUtilsService,
cryptoService,
validationService,
modalService,
logService,
searchPipe,
userNamePipe,
stateService
);
} }
ngOnInit() { ngOnInit() {
this.route.parent.params.subscribe(async (params) => { this.route.parent.params.subscribe(async params => {
this.providerId = params.providerId; this.providerId = params.providerId;
const provider = await this.providerService.get(this.providerId); const provider = await this.userService.getProvider(this.providerId);
if (!provider.canManageUsers) { if (!provider.canManageUsers) {
this.router.navigate(["../"], { relativeTo: this.route }); this.router.navigate(['../'], { relativeTo: this.route });
return; return;
} }
@@ -100,10 +83,10 @@ export class PeopleComponent
await this.load(); await this.load();
this.route.queryParams.pipe(first()).subscribe(async (qParams) => { this.route.queryParams.pipe(first()).subscribe(async qParams => {
this.searchText = qParams.search; this.searchText = qParams.search;
if (qParams.viewEvents != null) { if (qParams.viewEvents != null) {
const user = this.users.filter((u) => u.id === qParams.viewEvents); const user = this.users.filter(u => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) { if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
this.events(user[0]); this.events(user[0]);
} }
@@ -133,10 +116,7 @@ export class PeopleComponent
} }
async edit(user: ProviderUserUserDetailsResponse) { async edit(user: ProviderUserUserDetailsResponse) {
const [modal] = await this.modalService.openViewRef( const [modal] = await this.modalService.openViewRef(UserAddEditComponent, this.addEditModalRef, comp => {
UserAddEditComponent,
this.addEditModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(user); comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId; comp.providerId = this.providerId;
comp.providerUserId = user != null ? user.id : null; comp.providerUserId = user != null ? user.id : null;
@@ -148,17 +128,16 @@ export class PeopleComponent
modal.close(); modal.close();
this.removeUser(user); this.removeUser(user);
}); });
} });
);
} }
async events(user: ProviderUserUserDetailsResponse) { async events(user: ProviderUserUserDetailsResponse) {
await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, (comp) => { const [modal] = await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, comp => {
comp.name = this.userNamePipe.transform(user); comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId; comp.providerId = this.providerId;
comp.entityId = user.id; comp.entityId = user.id;
comp.showUser = false; comp.showUser = false;
comp.entity = "user"; comp.entity = 'user';
}); });
} }
@@ -167,14 +146,10 @@ export class PeopleComponent
return; return;
} }
const [modal] = await this.modalService.openViewRef( const [modal] = await this.modalService.openViewRef(BulkRemoveComponent, this.bulkRemoveModalRef, comp => {
BulkRemoveComponent,
this.bulkRemoveModalRef,
(comp) => {
comp.providerId = this.providerId; comp.providerId = this.providerId;
comp.users = this.getCheckedUsers(); comp.users = this.getCheckedUsers();
} });
);
await modal.onClosedPromise(); await modal.onClosedPromise();
await this.load(); await this.load();
@@ -186,26 +161,18 @@ export class PeopleComponent
} }
const users = this.getCheckedUsers(); const users = this.getCheckedUsers();
const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited); const filteredUsers = users.filter(u => u.status === ProviderUserStatusType.Invited);
if (filteredUsers.length <= 0) { if (filteredUsers.length <= 0) {
this.platformUtilsService.showToast( this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
"error", this.i18nService.t('noSelectedUsersApplicable'));
this.i18nService.t("errorOccurred"),
this.i18nService.t("noSelectedUsersApplicable")
);
return; return;
} }
try { try {
const request = new ProviderUserBulkRequest(filteredUsers.map((user) => user.id)); const request = new ProviderUserBulkRequest(filteredUsers.map(user => user.id));
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request); const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
this.showBulkStatus( this.showBulkStatus(users, filteredUsers, response, this.i18nService.t('bulkReinviteMessage'));
users,
filteredUsers,
response,
this.i18nService.t("bulkReinviteMessage")
);
} catch (e) { } catch (e) {
this.validationService.showError(e); this.validationService.showError(e);
} }
@@ -217,32 +184,21 @@ export class PeopleComponent
return; return;
} }
const [modal] = await this.modalService.openViewRef( const [modal] = await this.modalService.openViewRef(BulkConfirmComponent, this.bulkConfirmModalRef, comp => {
BulkConfirmComponent,
this.bulkConfirmModalRef,
(comp) => {
comp.providerId = this.providerId; comp.providerId = this.providerId;
comp.users = this.getCheckedUsers(); comp.users = this.getCheckedUsers();
} });
);
await modal.onClosedPromise(); await modal.onClosedPromise();
await this.load(); await this.load();
} }
private async showBulkStatus( private async showBulkStatus(users: ProviderUserUserDetailsResponse[], filteredUsers: ProviderUserUserDetailsResponse[],
users: ProviderUserUserDetailsResponse[], request: Promise<ListResponse<ProviderUserBulkResponse>>, successfullMessage: string) {
filteredUsers: ProviderUserUserDetailsResponse[],
request: Promise<ListResponse<ProviderUserBulkResponse>>, const [modal, childComponent] = await this.modalService.openViewRef(BulkStatusComponent, this.bulkStatusModalRef, comp => {
successfullMessage: string
) {
const [modal, childComponent] = await this.modalService.openViewRef(
BulkStatusComponent,
this.bulkStatusModalRef,
(comp) => {
comp.loading = true; comp.loading = true;
} });
);
// Workaround to handle closing the modal shortly after it has been opened // Workaround to handle closing the modal shortly after it has been opened
let close = false; let close = false;
@@ -256,21 +212,18 @@ export class PeopleComponent
const response = await request; const response = await request;
if (modal) { if (modal) {
const keyedErrors: any = response.data const keyedErrors: any = response.data.filter(r => r.error !== '').reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
.filter((r) => r.error !== "")
.reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {}); const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
childComponent.users = users.map((user) => { childComponent.users = users.map(user => {
let message = keyedErrors[user.id] ?? successfullMessage; let message = keyedErrors[user.id] ?? successfullMessage;
// eslint-disable-next-line
if (!keyedFilteredUsers.hasOwnProperty(user.id)) { if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
message = this.i18nService.t("bulkFilteredMessage"); message = this.i18nService.t('bulkFilteredMessage');
} }
return { return {
user: user, user: user,
error: keyedErrors.hasOwnProperty(user.id), // eslint-disable-line error: keyedErrors.hasOwnProperty(user.id),
message: message, message: message,
}; };
}); });

View File

@@ -1,121 +1,68 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle"> <div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document"> <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form <form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
class="modal-content"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title" id="userAddEditTitle"> <h2 class="modal-title" id="userAddEditTitle">
{{ title }} {{title}}
<small class="text-muted" *ngIf="name">{{ name }}</small> <small class="text-muted" *ngIf="name">{{name}}</small>
</h2> </h2>
<button <button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
type="button"
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body" *ngIf="loading"> <div class="modal-body" *ngIf="loading">
<i <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div> </div>
<div class="modal-body" *ngIf="!loading"> <div class="modal-body" *ngIf="!loading">
<ng-container *ngIf="!editMode"> <ng-container *ngIf="!editMode">
<p>{{ "providerInviteUserDesc" | i18n }}</p> <p>{{'providerInviteUserDesc' | i18n}}</p>
<div class="form-group mb-4"> <div class="form-group mb-4">
<label for="emails">{{ "email" | i18n }}</label> <label for="emails">{{'email' | i18n}}</label>
<input <input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required
id="emails" appAutoFocus>
class="form-control" <small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small>
type="text"
name="Emails"
[(ngModel)]="emails"
required
appAutoFocus
/>
<small class="text-muted">{{ "inviteMultipleEmailDesc" | i18n: "20" }}</small>
</div> </div>
</ng-container> </ng-container>
<h3> <h3>
{{ "userType" | i18n }} {{'userType' | i18n}}
<a <a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
target="_blank" href="https://bitwarden.com/help/article/user-types-access-control/#user-types">
rel="noopener" <i class="fa fa-question-circle-o" aria-hidden="true"></i>
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/provider-users/"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a> </a>
</h3> </h3>
<div class="form-check mt-2 form-check-block"> <div class="form-check mt-2 form-check-block">
<input <input class="form-check-input" type="radio" name="userType" id="userTypeServiceUser"
class="form-check-input" [value]="userType.ServiceUser" [(ngModel)]="type">
type="radio"
name="userType"
id="userTypeServiceUser"
[value]="userType.ServiceUser"
[(ngModel)]="type"
/>
<label class="form-check-label" for="userTypeServiceUser"> <label class="form-check-label" for="userTypeServiceUser">
{{ "serviceUser" | i18n }} {{'serviceUser' | i18n}}
<small>{{ "serviceUserDesc" | i18n }}</small> <small>{{'serviceUserDesc' | i18n}}</small>
</label> </label>
</div> </div>
<div class="form-check mt-2 form-check-block"> <div class="form-check mt-2 form-check-block">
<input <input class="form-check-input" type="radio" name="userType" id="userTypeProviderAdmin"
class="form-check-input" [value]="userType.ProviderAdmin" [(ngModel)]="type">
type="radio"
name="userType"
id="userTypeProviderAdmin"
[value]="userType.ProviderAdmin"
[(ngModel)]="type"
/>
<label class="form-check-label" for="userTypeProviderAdmin"> <label class="form-check-label" for="userTypeProviderAdmin">
{{ "providerAdmin" | i18n }} {{'providerAdmin' | i18n}}
<small>{{ "providerAdminDesc" | i18n }}</small> <small>{{'providerAdminDesc' | i18n}}</small>
</label> </label>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span> <span>{{'save' | i18n}}</span>
</button> </button>
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal"> <button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "cancel" | i18n }} {{'cancel' | i18n}}
</button> </button>
<div class="ml-auto"> <div class="ml-auto">
<button <button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
#deleteBtn appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading"
type="button" [appApiAction]="deletePromise">
(click)="delete()" <i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i>
class="btn btn-outline-danger" <i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading"
appA11yTitle="{{ 'delete' | i18n }}" title="{{'loading' | i18n}}" aria-hidden="true"></i>
*ngIf="editMode"
[disabled]="deleteBtn.loading"
[appApiAction]="deletePromise"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="deleteBtn.loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!deleteBtn.loading"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,17 +1,28 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { ApiService } from "jslib-common/abstractions/api.service"; import { ToasterService } from 'angular2-toaster';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { ProviderUserType } from "jslib-common/enums/providerUserType"; import { LogService } from 'jslib-common/abstractions/log.service';
import { PermissionsApi } from "jslib-common/models/api/permissionsApi"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { ProviderUserInviteRequest } from "jslib-common/models/request/provider/providerUserInviteRequest";
import { ProviderUserUpdateRequest } from "jslib-common/models/request/provider/providerUserUpdateRequest"; import { ProviderUserInviteRequest } from 'jslib-common/models/request/provider/providerUserInviteRequest';
import { PermissionsApi } from 'jslib-common/models/api/permissionsApi';
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { ProviderUserUpdateRequest } from 'jslib-common/models/request/provider/providerUserUpdateRequest';
@Component({ @Component({
selector: "provider-user-add-edit", selector: 'provider-user-add-edit',
templateUrl: "user-add-edit.component.html", templateUrl: 'user-add-edit.component.html',
}) })
export class UserAddEditComponent implements OnInit { export class UserAddEditComponent implements OnInit {
@Input() name: string; @Input() name: string;
@@ -21,30 +32,27 @@ export class UserAddEditComponent implements OnInit {
@Output() onDeletedUser = new EventEmitter(); @Output() onDeletedUser = new EventEmitter();
loading = true; loading = true;
editMode = false; editMode: boolean = false;
title: string; title: string;
emails: string; emails: string;
type: ProviderUserType = ProviderUserType.ServiceUser; type: ProviderUserType = ProviderUserType.ServiceUser;
permissions = new PermissionsApi(); permissions = new PermissionsApi();
showCustom = false; showCustom = false;
access: "all" | "selected" = "selected"; access: 'all' | 'selected' = 'selected';
formPromise: Promise<any>; formPromise: Promise<any>;
deletePromise: Promise<any>; deletePromise: Promise<any>;
userType = ProviderUserType; userType = ProviderUserType;
constructor( constructor(private apiService: ApiService, private i18nService: I18nService,
private apiService: ApiService, private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private logService: LogService) { }
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() { async ngOnInit() {
this.editMode = this.loading = this.providerUserId != null; this.editMode = this.loading = this.providerUserId != null;
if (this.editMode) { if (this.editMode) {
this.editMode = true; this.editMode = true;
this.title = this.i18nService.t("editUser"); this.title = this.i18nService.t('editUser');
try { try {
const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId); const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId);
this.type = user.type; this.type = user.type;
@@ -52,7 +60,7 @@ export class UserAddEditComponent implements OnInit {
this.logService.error(e); this.logService.error(e);
} }
} else { } else {
this.title = this.i18nService.t("inviteUser"); this.title = this.i18nService.t('inviteUser');
} }
this.loading = false; this.loading = false;
@@ -63,11 +71,7 @@ export class UserAddEditComponent implements OnInit {
if (this.editMode) { if (this.editMode) {
const request = new ProviderUserUpdateRequest(); const request = new ProviderUserUpdateRequest();
request.type = this.type; request.type = this.type;
this.formPromise = this.apiService.putProviderUser( this.formPromise = this.apiService.putProviderUser(this.providerId, this.providerUserId, request);
this.providerId,
this.providerUserId,
request
);
} else { } else {
const request = new ProviderUserInviteRequest(); const request = new ProviderUserInviteRequest();
request.emails = this.emails.trim().split(/\s*,\s*/); request.emails = this.emails.trim().split(/\s*,\s*/);
@@ -75,11 +79,8 @@ export class UserAddEditComponent implements OnInit {
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request); this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
} }
await this.formPromise; await this.formPromise;
this.platformUtilsService.showToast( this.toasterService.popAsync('success', null,
"success", this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
null,
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
);
this.onSavedUser.emit(); this.onSavedUser.emit();
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
@@ -92,12 +93,8 @@ export class UserAddEditComponent implements OnInit {
} }
const confirmed = await this.platformUtilsService.showDialog( const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("removeUserConfirmation"), this.i18nService.t('removeUserConfirmation'), this.name,
this.name, this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) { if (!confirmed) {
return false; return false;
} }
@@ -105,14 +102,11 @@ export class UserAddEditComponent implements OnInit {
try { try {
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId); this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
await this.deletePromise; await this.deletePromise;
this.platformUtilsService.showToast( this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.name));
"success",
null,
this.i18nService.t("removedUserId", this.name)
);
this.onDeletedUser.emit(); this.onDeletedUser.emit();
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
} }
} }

View File

@@ -5,33 +5,33 @@
<div class="my-auto d-flex align-items-center pl-1"> <div class="my-auto d-flex align-items-center pl-1">
<app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar> <app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar>
<div class="org-name ml-3"> <div class="org-name ml-3">
<span>{{ provider.name }}</span> <span>{{provider.name}}</span>
<small class="text-muted">{{ "provider" | i18n }}</small> <small class="text-muted">{{'provider' | i18n}}</small>
</div> </div>
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled"> <div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
<div class="card-body py-2"> <div class="card-body py-2">
<i class="bwi bwi-exclamation-triangle" aria-hidden="true"></i> <i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
{{ "providerIsDisabled" | i18n }} {{'providerIsDisabled' | i18n}}
</div> </div>
</div> </div>
</div> </div>
<ul class="nav nav-tabs" *ngIf="showMenuBar"> <ul class="nav nav-tabs" *ngIf="showMenuBar">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" routerLink="clients" routerLinkActive="active"> <a class="nav-link" routerLink="clients" routerLinkActive="active">
<i class="bwi bwi-bank" aria-hidden="true"></i> <i class="fa fa-university" aria-hidden="true"></i>
{{ "clients" | i18n }} {{'clients' | i18n}}
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="showManageTab"> <li class="nav-item" *ngIf="showManageTab">
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active"> <a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
<i class="bwi bwi-sliders" aria-hidden="true"></i> <i class="fa fa-sliders" aria-hidden="true"></i>
{{ "manage" | i18n }} {{'manage' | i18n}}
</a> </a>
</li> </li>
<li class="nav-item" *ngIf="showSettingsTab"> <li class="nav-item" *ngIf="showSettingsTab">
<a class="nav-link" routerLink="settings" routerLinkActive="active"> <a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="bwi bwi-cogs" aria-hidden="true"></i> <i class="fa fa-cogs" aria-hidden="true"></i>
{{ "settings" | i18n }} {{'settings' | i18n}}
</a> </a>
</li> </li>
</ul> </ul>

View File

@@ -1,29 +1,31 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from '@angular/router';
import { ProviderService } from "jslib-common/abstractions/provider.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { Provider } from "jslib-common/models/domain/provider";
import { Provider } from 'jslib-common/models/domain/provider';
@Component({ @Component({
selector: "providers-layout", selector: 'providers-layout',
templateUrl: "providers-layout.component.html", templateUrl: 'providers-layout.component.html',
}) })
export class ProvidersLayoutComponent { export class ProvidersLayoutComponent {
provider: Provider; provider: Provider;
private providerId: string; private providerId: string;
constructor(private route: ActivatedRoute, private providerService: ProviderService) {} constructor(private route: ActivatedRoute, private userService: UserService) { }
ngOnInit() { ngOnInit() {
document.body.classList.remove("layout_frontend"); document.body.classList.remove('layout_frontend');
this.route.params.subscribe(async (params) => { this.route.params.subscribe(async params => {
this.providerId = params.providerId; this.providerId = params.providerId;
await this.load(); await this.load();
}); });
} }
async load() { async load() {
this.provider = await this.providerService.get(this.providerId); this.provider = await this.userService.getProvider(this.providerId);
} }
get showMenuBar() { get showMenuBar() {
@@ -41,9 +43,9 @@ export class ProvidersLayoutComponent {
get manageRoute(): string { get manageRoute(): string {
switch (true) { switch (true) {
case this.provider.canManageUsers: case this.provider.canManageUsers:
return "manage/people"; return 'manage/people';
case this.provider.canAccessEventLogs: case this.provider.canAccessEventLogs:
return "manage/events"; return 'manage/events';
} }
} }
} }

View File

@@ -1,108 +1,110 @@
import { NgModule } from "@angular/core"; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from "jslib-angular/guards/auth.guard"; import { AuthGuardService } from 'jslib-angular/services/auth-guard.service';
import { Permissions } from "jslib-common/enums/permissions"; import { Permissions } from 'jslib-common/enums/permissions';
import { FrontendLayoutComponent } from "src/app/layouts/frontend-layout.component"; import { AddOrganizationComponent } from './clients/add-organization.component';
import { ProvidersComponent } from "src/app/providers/providers.component"; import { ClientsComponent } from './clients/clients.component';
import { CreateOrganizationComponent } from './clients/create-organization.component';
import { AcceptProviderComponent } from './manage/accept-provider.component';
import { EventsComponent } from './manage/events.component';
import { ManageComponent } from './manage/manage.component';
import { PeopleComponent } from './manage/people.component';
import { ProvidersLayoutComponent } from './providers-layout.component';
import { SettingsComponent } from './settings/settings.component';
import { SetupProviderComponent } from './setup/setup-provider.component';
import { SetupComponent } from './setup/setup.component';
import { ClientsComponent } from "./clients/clients.component"; import { FrontendLayoutComponent } from 'src/app/layouts/frontend-layout.component';
import { CreateOrganizationComponent } from "./clients/create-organization.component";
import { PermissionsGuard } from "./guards/provider-type.guard"; import { ProvidersComponent } from 'src/app/providers/providers.component';
import { ProviderGuard } from "./guards/provider.guard"; import { ProviderGuardService } from './services/provider-guard.service';
import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { ProviderTypeGuardService } from './services/provider-type-guard.service';
import { EventsComponent } from "./manage/events.component"; import { AccountComponent } from './settings/account.component';
import { ManageComponent } from "./manage/manage.component";
import { PeopleComponent } from "./manage/people.component";
import { ProvidersLayoutComponent } from "./providers-layout.component";
import { AccountComponent } from "./settings/account.component";
import { SettingsComponent } from "./settings/settings.component";
import { SetupProviderComponent } from "./setup/setup-provider.component";
import { SetupComponent } from "./setup/setup.component";
const routes: Routes = [ const routes: Routes = [
{ {
path: "", path: '',
canActivate: [AuthGuard], canActivate: [AuthGuardService],
component: ProvidersComponent, component: ProvidersComponent,
}, },
{ {
path: "", path: '',
component: FrontendLayoutComponent, component: FrontendLayoutComponent,
children: [ children: [
{ {
path: "setup-provider", path: 'setup-provider',
component: SetupProviderComponent, component: SetupProviderComponent,
data: { titleId: "setupProvider" }, data: { titleId: 'setupProvider' },
}, },
{ {
path: "accept-provider", path: 'accept-provider',
component: AcceptProviderComponent, component: AcceptProviderComponent,
data: { titleId: "acceptProvider" }, data: { titleId: 'acceptProvider' },
}, },
], ],
}, },
{ {
path: "", path: '',
canActivate: [AuthGuard], canActivate: [AuthGuardService],
children: [ children: [
{ {
path: "setup", path: 'setup',
component: SetupComponent, component: SetupComponent,
}, },
{ {
path: ":providerId", path: ':providerId',
component: ProvidersLayoutComponent, component: ProvidersLayoutComponent,
canActivate: [ProviderGuard], canActivate: [ProviderGuardService],
children: [ children: [
{ path: "", pathMatch: "full", redirectTo: "clients" }, { path: '', pathMatch: 'full', redirectTo: 'clients' },
{ path: "clients/create", component: CreateOrganizationComponent }, { path: 'clients/create', component: CreateOrganizationComponent },
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } }, { path: 'clients', component: ClientsComponent, data: { titleId: 'clients' } },
{ {
path: "manage", path: 'manage',
component: ManageComponent, component: ManageComponent,
children: [ children: [
{ {
path: "", path: '',
pathMatch: "full", pathMatch: 'full',
redirectTo: "people", redirectTo: 'people',
}, },
{ {
path: "people", path: 'people',
component: PeopleComponent, component: PeopleComponent,
canActivate: [PermissionsGuard], canActivate: [ProviderTypeGuardService],
data: { data: {
titleId: "people", titleId: 'people',
permissions: [Permissions.ManageUsers], permissions: [Permissions.ManageUsers],
}, },
}, },
{ {
path: "events", path: 'events',
component: EventsComponent, component: EventsComponent,
canActivate: [PermissionsGuard], canActivate: [ProviderTypeGuardService],
data: { data: {
titleId: "eventLogs", titleId: 'eventLogs',
permissions: [Permissions.AccessEventLogs], permissions: [Permissions.AccessEventLogs],
}, },
}, },
], ],
}, },
{ {
path: "settings", path: 'settings',
component: SettingsComponent, component: SettingsComponent,
children: [ children: [
{ {
path: "", path: '',
pathMatch: "full", pathMatch: 'full',
redirectTo: "account", redirectTo: 'account',
}, },
{ {
path: "account", path: 'account',
component: AccountComponent, component: AccountComponent,
canActivate: [PermissionsGuard], canActivate: [ProviderTypeGuardService],
data: { data: {
titleId: "myProvider", titleId: 'myProvider',
permissions: [Permissions.ManageProvider], permissions: [Permissions.ManageProvider],
}, },
}, },
@@ -118,4 +120,4 @@ const routes: Routes = [
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],
}) })
export class ProvidersRoutingModule {} export class ProvidersRoutingModule { }

View File

@@ -1,34 +1,44 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from '@angular/common';
import { ComponentFactoryResolver, NgModule } from "@angular/core"; import { ComponentFactoryResolver } from '@angular/core';
import { FormsModule } from "@angular/forms"; import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { JslibModule } from "jslib-angular/jslib.module"; import { ModalService } from 'jslib-angular/services/modal.service';
import { ModalService } from "jslib-angular/services/modal.service";
import { OssModule } from "src/app/oss.module"; import { ProviderGuardService } from './services/provider-guard.service';
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
import { ProviderService } from './services/provider.service';
import { AddOrganizationComponent } from "./clients/add-organization.component"; import { ProvidersLayoutComponent } from './providers-layout.component';
import { ClientsComponent } from "./clients/clients.component"; import { ProvidersRoutingModule } from './providers-routing.module';
import { CreateOrganizationComponent } from "./clients/create-organization.component";
import { PermissionsGuard } from "./guards/provider-type.guard"; import { AddOrganizationComponent } from './clients/add-organization.component';
import { ProviderGuard } from "./guards/provider.guard"; import { ClientsComponent } from './clients/clients.component';
import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { CreateOrganizationComponent } from './clients/create-organization.component';
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component"; import { AcceptProviderComponent } from './manage/accept-provider.component';
import { EventsComponent } from "./manage/events.component"; import { BulkConfirmComponent } from './manage/bulk/bulk-confirm.component';
import { ManageComponent } from "./manage/manage.component"; import { BulkRemoveComponent } from './manage/bulk/bulk-remove.component';
import { PeopleComponent } from "./manage/people.component"; import { EventsComponent } from './manage/events.component';
import { UserAddEditComponent } from "./manage/user-add-edit.component"; import { ManageComponent } from './manage/manage.component';
import { ProvidersLayoutComponent } from "./providers-layout.component"; import { PeopleComponent } from './manage/people.component';
import { ProvidersRoutingModule } from "./providers-routing.module"; import { UserAddEditComponent } from './manage/user-add-edit.component';
import { WebProviderService } from "./services/webProvider.service";
import { AccountComponent } from "./settings/account.component"; import { AccountComponent } from './settings/account.component';
import { SettingsComponent } from "./settings/settings.component"; import { SettingsComponent } from './settings/settings.component';
import { SetupProviderComponent } from "./setup/setup-provider.component";
import { SetupComponent } from "./setup/setup.component"; import { SetupProviderComponent } from './setup/setup-provider.component';
import { SetupComponent } from './setup/setup.component';
import { OssModule } from 'src/app/oss.module';
@NgModule({ @NgModule({
imports: [CommonModule, FormsModule, OssModule, JslibModule, ProvidersRoutingModule], imports: [
CommonModule,
FormsModule,
OssModule,
ProvidersRoutingModule,
],
declarations: [ declarations: [
AcceptProviderComponent, AcceptProviderComponent,
AccountComponent, AccountComponent,
@@ -46,13 +56,14 @@ import { SetupComponent } from "./setup/setup.component";
SetupProviderComponent, SetupProviderComponent,
UserAddEditComponent, UserAddEditComponent,
], ],
providers: [WebProviderService, ProviderGuard, PermissionsGuard], providers: [
ProviderService,
ProviderGuardService,
ProviderTypeGuardService,
],
}) })
export class ProvidersModule { export class ProvidersModule {
constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) { constructor(modalService: ModalService, componentFactoryResolver: ComponentFactoryResolver) {
modalService.registerComponentFactoryResolver( modalService.registerComponentFactoryResolver(AddOrganizationComponent, componentFactoryResolver);
AddOrganizationComponent,
componentFactoryResolver
);
} }
} }

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
} from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { UserService } from 'jslib-common/abstractions/user.service';
@Injectable()
export class ProviderGuardService implements CanActivate {
constructor(private userService: UserService, private router: Router,
private toasterService: ToasterService, private i18nService: I18nService) { }
async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.userService.getProvider(route.params.providerId);
if (provider == null) {
this.router.navigate(['/']);
return false;
}
if (!provider.isProviderAdmin && !provider.enabled) {
this.toasterService.popAsync('error', null, this.i18nService.t('providerIsDisabled'));
this.router.navigate(['/']);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,31 @@
import { Injectable } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
} from '@angular/router';
import { UserService } from 'jslib-common/abstractions/user.service';
import { Permissions } from 'jslib-common/enums/permissions';
@Injectable()
export class ProviderTypeGuardService implements CanActivate {
constructor(private userService: UserService, private router: Router) { }
async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.userService.getProvider(route.params.providerId);
const permissions = route.data == null ? null : route.data.permissions as Permissions[];
if (
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) ||
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) ||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers)
) {
return true;
}
this.router.navigate(['/providers', provider.id]);
return false;
}
}

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ProviderAddOrganizationRequest } from 'jslib-common/models/request/provider/providerAddOrganizationRequest';
@Injectable()
export class ProviderService {
constructor(private cryptoService: CryptoService, private syncService: SyncService, private apiService: ApiService) {}
async addOrganizationToProvider(providerId: string, organizationId: string) {
const orgKey = await this.cryptoService.getOrgKey(organizationId);
const providerKey = await this.cryptoService.getProviderKey(providerId);
const encryptedOrgKey = await this.cryptoService.encrypt(orgKey.key, providerKey);
const request = new ProviderAddOrganizationRequest();
request.organizationId = organizationId;
request.key = encryptedOrgKey.encryptedString;
const response = await this.apiService.postProviderAddOrganization(providerId, request);
await this.syncService.fullSync(true);
return response;
}
async detachOrganizastion(providerId: string, organizationId: string): Promise<any> {
await this.apiService.deleteProviderOrganization(providerId, organizationId);
await this.syncService.fullSync(true);
}
}

View File

@@ -1,35 +0,0 @@
import { Injectable } from "@angular/core";
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { SyncService } from "jslib-common/abstractions/sync.service";
import { ProviderAddOrganizationRequest } from "jslib-common/models/request/provider/providerAddOrganizationRequest";
@Injectable()
export class WebProviderService {
constructor(
private cryptoService: CryptoService,
private syncService: SyncService,
private apiService: ApiService
) {}
async addOrganizationToProvider(providerId: string, organizationId: string) {
const orgKey = await this.cryptoService.getOrgKey(organizationId);
const providerKey = await this.cryptoService.getProviderKey(providerId);
const encryptedOrgKey = await this.cryptoService.encrypt(orgKey.key, providerKey);
const request = new ProviderAddOrganizationRequest();
request.organizationId = organizationId;
request.key = encryptedOrgKey.encryptedString;
const response = await this.apiService.postProviderAddOrganization(providerId, request);
await this.syncService.fullSync(true);
return response;
}
async detachOrganizastion(providerId: string, organizationId: string): Promise<any> {
await this.apiService.deleteProviderOrganization(providerId, organizationId);
await this.syncService.fullSync(true);
}
}

View File

@@ -1,52 +1,30 @@
<div class="page-header"> <div class="page-header">
<h1>{{ "myProvider" | i18n }}</h1> <h1>{{'myProvider' | i18n}}</h1>
</div> </div>
<div *ngIf="loading"> <div *ngIf="loading">
<i <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</div> </div>
<form <form *ngIf="provider && !loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
*ngIf="provider && !loading"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
>
<div class="row"> <div class="row">
<div class="col-6"> <div class="col-6">
<div class="form-group"> <div class="form-group">
<label for="name">{{ "providerName" | i18n }}</label> <label for="name">{{'providerName' | i18n}}</label>
<input <input id="name" class="form-control" type="text" name="Name" [(ngModel)]="provider.name"
id="name" [disabled]="selfHosted">
class="form-control"
type="text"
name="Name"
[(ngModel)]="provider.name"
[disabled]="selfHosted"
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label> <label for="billingEmail">{{'billingEmail' | i18n}}</label>
<input <input id="billingEmail" class="form-control" type="text" name="BillingEmail"
id="billingEmail" [(ngModel)]="provider.billingEmail" [disabled]="selfHosted">
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="provider.billingEmail"
[disabled]="selfHosted"
/>
</div> </div>
</div> </div>
<div class="col-6"> <div class="col-6">
<app-avatar data="{{ provider.name }}" dynamic="true" size="75" fontSize="35"></app-avatar> <app-avatar data="{{provider.name}}" dynamic="true" size="75" fontSize="35"></app-avatar>
</div> </div>
</div> </div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span> <span>{{'save' | i18n}}</span>
</button> </button>
</form> </form>

View File

@@ -1,17 +1,20 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { ApiService } from "jslib-common/abstractions/api.service"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from "jslib-common/abstractions/log.service"; import { LogService } from 'jslib-common/abstractions/log.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { SyncService } from "jslib-common/abstractions/sync.service"; import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ProviderUpdateRequest } from "jslib-common/models/request/provider/providerUpdateRequest";
import { ProviderResponse } from "jslib-common/models/response/provider/providerResponse"; import { ProviderUpdateRequest } from 'jslib-common/models/request/provider/providerUpdateRequest';
import { ProviderResponse } from 'jslib-common/models/response/provider/providerResponse';
@Component({ @Component({
selector: "provider-account", selector: 'provider-account',
templateUrl: "account.component.html", templateUrl: 'account.component.html',
}) })
export class AccountComponent { export class AccountComponent {
selfHosted = false; selfHosted = false;
@@ -22,18 +25,14 @@ export class AccountComponent {
private providerId: string; private providerId: string;
constructor( constructor(private apiService: ApiService, private i18nService: I18nService,
private apiService: ApiService, private toasterService: ToasterService, private route: ActivatedRoute,
private i18nService: I18nService, private syncService: SyncService, private platformUtilsService: PlatformUtilsService,
private route: ActivatedRoute, private logService: LogService) { }
private syncService: SyncService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() { async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost(); this.selfHosted = this.platformUtilsService.isSelfHost();
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async params => {
this.providerId = params.providerId; this.providerId = params.providerId;
try { try {
this.provider = await this.apiService.getProvider(this.providerId); this.provider = await this.apiService.getProvider(this.providerId);
@@ -55,7 +54,7 @@ export class AccountComponent {
return this.syncService.fullSync(true); return this.syncService.fullSync(true);
}); });
await this.formPromise; await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerUpdated")); this.toasterService.popAsync('success', null, this.i18nService.t('providerUpdated'));
} catch (e) { } catch (e) {
this.logService.error(`Handled exception: ${e}`); this.logService.error(`Handled exception: ${e}`);
} }

View File

@@ -2,10 +2,10 @@
<div class="row"> <div class="row">
<div class="col-3"> <div class="col-3">
<div class="card"> <div class="card">
<div class="card-header">{{ "settings" | i18n }}</div> <div class="card-header">{{'settings' | i18n}}</div>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<a routerLink="account" class="list-group-item" routerLinkActive="active"> <a routerLink="account" class="list-group-item" routerLinkActive="active">
{{ "myProvider" | i18n }} {{'myProvider' | i18n}}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,18 +1,20 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from '@angular/router';
import { ProviderService } from "jslib-common/abstractions/provider.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
@Component({ @Component({
selector: "provider-settings", selector: 'provider-settings',
templateUrl: "settings.component.html", templateUrl: 'settings.component.html',
}) })
export class SettingsComponent { export class SettingsComponent {
constructor(private route: ActivatedRoute, private providerService: ProviderService) {} constructor(private route: ActivatedRoute, private userService: UserService,
private platformUtilsService: PlatformUtilsService) { }
ngOnInit() { ngOnInit() {
this.route.parent.params.subscribe(async (params) => { this.route.parent.params.subscribe(async params => {
await this.providerService.get(params.providerId); const provider = await this.userService.getProvider(params.providerId);
}); });
} }
} }

View File

@@ -1,31 +1,23 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading"> <div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div> <div>
<img class="mb-4 logo logo-themed" alt="Bitwarden" /> <img class="mb-4 logo logo-themed" alt="Bitwarden">
<p class="text-center"> <p class="text-center">
<i <i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin bwi-2x text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p> </p>
</div> </div>
</div> </div>
<div class="container" *ngIf="!loading && !authed"> <div class="container" *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="lead text-center mb-4">{{ "setupProvider" | i18n }}</p> <p class="lead text-center mb-4">{{'setupProvider' | i18n}}</p>
<div class="card d-block"> <div class="card d-block">
<div class="card-body"> <div class="card-body">
<p>{{ "setupProviderLoginDesc" | i18n }}</p> <p>{{'setupProviderLoginDesc' | i18n}}</p>
<hr /> <hr>
<div class="d-flex"> <div class="d-flex">
<a <a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
routerLink="/login" {{'logIn' | i18n}}
[queryParams]="{ email: email }"
class="btn btn-primary btn-block"
>
{{ "logIn" | i18n }}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,22 +1,22 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { BaseAcceptComponent } from "src/app/common/base.accept.component"; import { BaseAcceptComponent } from 'src/app/common/base.accept.component';
@Component({ @Component({
selector: "app-setup-provider", selector: 'app-setup-provider',
templateUrl: "setup-provider.component.html", templateUrl: 'setup-provider.component.html',
}) })
export class SetupProviderComponent extends BaseAcceptComponent { export class SetupProviderComponent extends BaseAcceptComponent {
failedShortMessage = "inviteAcceptFailedShort";
failedMessage = "inviteAcceptFailed";
requiredParameters = ["providerId", "email", "token"]; failedShortMessage = 'inviteAcceptFailedShort';
failedMessage = 'inviteAcceptFailed';
requiredParameters = ['providerId', 'email', 'token'];
async authedHandler(qParams: any) { async authedHandler(qParams: any) {
this.router.navigate(["/providers/setup"], { queryParams: qParams }); this.router.navigate(['/providers/setup'], {queryParams: qParams});
} }
async unauthedHandler(qParams: any) { // tslint:disable-next-line
// Empty async unauthedHandler(qParams: any) {}
}
} }

View File

@@ -1,37 +1,30 @@
<app-navbar></app-navbar> <app-navbar></app-navbar>
<div class="container page-content"> <div class="container page-content">
<div class="page-header"> <div class="page-header">
<h1>{{ "setupProvider" | i18n }}</h1> <h1>{{'setupProvider' | i18n}}</h1>
</div> </div>
<p>{{ "setupProviderDesc" | i18n }}</p> <p>{{'setupProviderDesc' | i18n}}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="loading">
<h2 class="mt-5">{{ "generalInformation" | i18n }}</h2> <h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
<div class="row"> <div class="row">
<div class="form-group col-6"> <div class="form-group col-6">
<label for="name">{{ "providerName" | i18n }}</label> <label for="name">{{'providerName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required /> <input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
</div> </div>
<div class="form-group col-6"> <div class="form-group col-6">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label> <label for="billingEmail">{{'billingEmail' | i18n}}</label>
<input <input id="billingEmail" class="form-control" type="text" name="BillingEmail" [(ngModel)]="billingEmail" required>
id="billingEmail"
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="billingEmail"
required
/>
</div> </div>
</div> </div>
<div class="mt-4"> <div class="mt-4">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{ "submit" | i18n }}</span> <span>{{'submit' | i18n}}</span>
</button> </button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel"> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
{{ "cancel" | i18n }} {{'cancel' | i18n}}
</button> </button>
</div> </div>
</form> </form>

View File

@@ -1,18 +1,29 @@
import { Component, OnInit } from "@angular/core"; import {
import { ActivatedRoute, Router } from "@angular/router"; Component,
import { first } from "rxjs/operators"; OnInit,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import {
Toast,
ToasterService,
} from 'angular2-toaster';
import { ValidationService } from "jslib-angular/services/validation.service"; import { first } from 'rxjs/operators';
import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SyncService } from "jslib-common/abstractions/sync.service"; import { ValidationService } from 'jslib-angular/services/validation.service';
import { ProviderSetupRequest } from "jslib-common/models/request/provider/providerSetupRequest"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ProviderSetupRequest } from 'jslib-common/models/request/provider/providerSetupRequest';
@Component({ @Component({
selector: "provider-setup", selector: 'provider-setup',
templateUrl: "setup.component.html", templateUrl: 'setup.component.html',
}) })
export class SetupComponent implements OnInit { export class SetupComponent implements OnInit {
loading = true; loading = true;
@@ -25,30 +36,25 @@ export class SetupComponent implements OnInit {
name: string; name: string;
billingEmail: string; billingEmail: string;
constructor( constructor(private router: Router, private toasterService: ToasterService,
private router: Router, private i18nService: I18nService, private route: ActivatedRoute,
private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService, private apiService: ApiService,
private i18nService: I18nService, private syncService: SyncService, private validationService: ValidationService) { }
private route: ActivatedRoute,
private cryptoService: CryptoService,
private apiService: ApiService,
private syncService: SyncService,
private validationService: ValidationService
) {}
ngOnInit() { ngOnInit() {
document.body.classList.remove("layout_frontend"); document.body.classList.remove('layout_frontend');
this.route.queryParams.pipe(first()).subscribe(async (qParams) => { this.route.queryParams.pipe(first()).subscribe(async qParams => {
const error = qParams.providerId == null || qParams.email == null || qParams.token == null; const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
if (error) { if (error) {
this.platformUtilsService.showToast( const toast: Toast = {
"error", type: 'error',
null, title: null,
this.i18nService.t("emergencyInviteAcceptFailed"), body: this.i18nService.t('emergencyInviteAcceptFailed'),
{ timeout: 10000 } timeout: 10000,
); };
this.router.navigate(["/"]); this.toasterService.popAsync(toast);
this.router.navigate(['/']);
return; return;
} }
@@ -59,11 +65,11 @@ export class SetupComponent implements OnInit {
try { try {
const provider = await this.apiService.getProvider(this.providerId); const provider = await this.apiService.getProvider(this.providerId);
if (provider.name != null) { if (provider.name != null) {
this.router.navigate(["/providers", provider.id], { replaceUrl: true }); this.router.navigate(['/providers', provider.id], { replaceUrl: true });
} }
} catch (e) { } catch (e) {
this.validationService.showError(e); this.validationService.showError(e);
this.router.navigate(["/"]); this.router.navigate(['/']);
} }
}); });
} }
@@ -86,10 +92,10 @@ export class SetupComponent implements OnInit {
request.key = key; request.key = key;
const provider = await this.apiService.postProviderSetup(this.providerId, request); const provider = await this.apiService.postProviderSetup(this.providerId, request);
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup")); this.toasterService.popAsync('success', null, this.i18nService.t('providerSetup'));
await this.syncService.fullSync(true); await this.syncService.fullSync(true);
this.router.navigate(["/providers", provider.id]); this.router.navigate(['/providers', provider.id]);
} catch (e) { } catch (e) {
this.validationService.showError(e); this.validationService.showError(e);
} }

View File

@@ -1,11 +1,11 @@
const { AngularWebpackPlugin } = require("@ngtools/webpack"); const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin;
const webpackConfig = require("../webpack.config"); const webpackConfig = require('../webpack.config');
webpackConfig.entry["app/main"] = "./bitwarden_license/src/app/main.ts"; webpackConfig.entry['app/main'] = './bitwarden_license/src/app/main.ts';
webpackConfig.plugins[webpackConfig.plugins.length - 1] = new AngularWebpackPlugin({ webpackConfig.plugins[webpackConfig.plugins.length -1] = new AngularCompilerPlugin({
tsConfigPath: "tsconfig.json", tsConfigPath: 'tsconfig.json',
entryModule: "bitwarden_license/src/app/app.module#AppModule", entryModule: 'bitwarden_license/src/app/app.module#AppModule',
sourceMap: true, sourceMap: true,
}); });

View File

@@ -1,12 +1,12 @@
function load(envName) { function load(envName) {
return { return {
...require("./config/base.json"), ...require('./config/base.json'),
...loadConfig(envName), ...loadConfig(envName),
...loadConfig("local"), ...loadConfig('local'),
dev: { dev: {
...require("./config/base.json").dev, ...require('./config/base.json').dev,
...loadConfig(envName).dev, ...loadConfig(envName).dev,
...loadConfig("local").dev, ...loadConfig('local').dev,
}, },
}; };
} }
@@ -24,7 +24,8 @@ function loadConfig(configName) {
} catch (e) { } catch (e) {
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") { if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
return {}; return {};
} else { }
else {
throw e; throw e;
} }
} }
@@ -32,5 +33,5 @@ function loadConfig(configName) {
module.exports = { module.exports = {
load, load,
log, log
}; };

View File

@@ -7,7 +7,6 @@
"buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr" "buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr"
}, },
"dev": { "dev": {
"port": 8080, "allowedHosts": []
"allowedHosts": "auto"
} }
} }

1
config/self-hosted.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -1,9 +0,0 @@
{
"dev": {
"proxyApi": "http://localhost:4001",
"proxyIdentity": "http://localhost:33657",
"proxyEvents": "http://localhost:46274",
"proxyNotifications": "http://localhost:61841",
"port": 8081
}
}

2
jslib

Submodule jslib updated: 3bf25edd3e...c65e7db6e0

20105
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bitwarden/web-vault", "name": "bitwarden-web",
"version": "2.28.1", "version": "2.25.0",
"license": "GPL-3.0", "license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web", "repository": "https://github.com/bitwarden/web",
"scripts": { "scripts": {
@@ -29,91 +29,60 @@
"dist:bit:selfhost": "npm run build:bit:selfhost:prod", "dist:bit:selfhost": "npm run build:bit:selfhost:prod",
"deploy": "npm run dist:bit && gh-pages -d build", "deploy": "npm run dist:bit && gh-pages -d build",
"deploy:dev": "npm run dist:bit && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git", "deploy:dev": "npm run dist:bit && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
"lint": "eslint . && prettier --check .", "lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' || true",
"lint:fix": "eslint . --fix", "lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix"
"prettier": "prettier --write .",
"prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler-cli": "^12.2.13", "@angular/compiler-cli": "^11.2.11",
"@ngtools/webpack": "^12.2.13", "@ngtools/webpack": "^11.2.10",
"@types/jquery": "^3.5.5", "@types/jquery": "^3.5.5",
"@types/node": "^16.11.12", "@types/node": "^14.17.2",
"@types/webcrypto": "^0.0.28", "@types/webcrypto": "^0.0.28",
"@types/webpack": "^5.28.0", "@types/webpack": "^4.4.27",
"@typescript-eslint/eslint-plugin": "^5.10.1", "clean-webpack-plugin": "^3.0.0",
"@typescript-eslint/parser": "^5.10.1", "copy-webpack-plugin": "^6.4.0",
"autoprefixer": "^10.4.2",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "^6.5.1", "css-loader": "^5.2.3",
"eslint": "^8.7.0", "del": "^6.0.0",
"eslint-config-prettier": "^8.3.0", "file-loader": "^6.2.0",
"eslint-import-resolver-typescript": "^2.5.0",
"eslint-plugin-import": "^2.25.4",
"gh-pages": "^3.1.0", "gh-pages": "^3.1.0",
"html-loader": "^3.0.1", "html-loader": "^1.3.2",
"html-webpack-injector": "1.1.4", "html-webpack-injector": "1.1.4",
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^4.5.1",
"husky": "^7.0.4", "mini-css-extract-plugin": "^1.5.0",
"lint-staged": "^12.1.2",
"mini-css-extract-plugin": "^2.4.5",
"postcss": "^8.4.6",
"postcss-loader": "^6.2.1",
"prettier": "2.5.1",
"process": "^0.11.10",
"rimraf": "^3.0.2",
"sass": "^1.32.10", "sass": "^1.32.10",
"sass-loader": "^12.4.0", "sass-loader": "^10.1.1",
"style-loader": "^3.3.1", "style-loader": "^2.0.0",
"tailwindcss": "^3.0.18", "tapable": "^1.1.3",
"terser-webpack-plugin": "^5.2.5", "terser-webpack-plugin": "^4.2.3",
"ts-loader": "^9.2.5", "ts-loader": "^8.1.0",
"typescript": "4.3.5", "tslint": "^6.1.3",
"util": "^0.12.4", "tslint-loader": "^3.5.4",
"webpack": "^5.64.4", "typescript": "4.1.5",
"webpack-cli": "^4.9.1", "webpack": "^4.46.0",
"webpack-dev-server": "^4.6.0" "webpack-cli": "^4.6.0",
"webpack-dev-server": "^3.11.2"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^12.2.13",
"@angular/cdk": "^12.2.13",
"@angular/common": "^12.2.13",
"@angular/compiler": "^12.2.13",
"@angular/core": "^12.2.13",
"@angular/forms": "^12.2.13",
"@angular/platform-browser": "^12.2.13",
"@angular/platform-browser-dynamic": "^12.2.13",
"@angular/router": "^12.2.13",
"@bitwarden/jslib-angular": "file:jslib/angular", "@bitwarden/jslib-angular": "file:jslib/angular",
"@bitwarden/jslib-common": "file:jslib/common", "@bitwarden/jslib-common": "file:jslib/common",
"angular2-toaster": "11.0.1",
"bootstrap": "4.6.0", "bootstrap": "4.6.0",
"braintree-web-drop-in": "1.33.1", "braintree-web-drop-in": "1.30.1",
"browser-hrtime": "^1.1.8", "browser-hrtime": "^1.1.8",
"core-js": "^3.11.0", "core-js": "^3.11.0",
"date-input-polyfill": "^2.14.0", "date-input-polyfill": "^2.14.0",
"font-awesome": "4.7.0",
"jquery": "3.6.0", "jquery": "3.6.0",
"jszip": "^3.7.1",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^10.0.1",
"ngx-toastr": "14.1.4",
"node-forge": "^1.3.1",
"popper.js": "1.16.1", "popper.js": "1.16.1",
"qrious": "4.0.2", "qrious": "4.0.2",
"rxjs": "^7.4.0",
"sweetalert2": "^10.16.6", "sweetalert2": "^10.16.6",
"webcrypto-shim": "0.1.7", "webcrypto-shim": "0.1.7",
"whatwg-fetch": "3.6.2", "whatwg-fetch": "3.6.2"
"zone.js": "0.11.4"
}, },
"engines": { "engines": {
"node": "~16", "node": "~14",
"npm": "~8" "npm": "~7"
},
"lint-staged": {
"./!(jslib)**": "prettier --ignore-unknown --write",
"*.ts": "eslint --fix",
"*.png": "node scripts/optimize.js"
} }
} }

View File

@@ -1,4 +0,0 @@
/* eslint-disable no-undef */
module.exports = {
plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")],
};

View File

@@ -1,21 +0,0 @@
const child_process = require("child_process");
const path = require("path");
const images = process.argv.slice(2);
images.forEach((img) => {
switch (img.split(".").pop()) {
case "png":
child_process.execSync(
`npx @squoosh/cli --oxipng {} --output-dir "${path.dirname(img)}" "${img}"`
);
break;
case "jpg":
child_process.execSync(
`npx @squoosh/cli --mozjpeg {"quality":85,"baseline":false,"arithmetic":false,"progressive":true,"optimize_coding":true,"smoothing":0,"color_space":3,"quant_table":3,"trellis_multipass":false,"trellis_opt_zero":false,"trellis_opt_table":false,"trellis_loops":1,"auto_subsample":true,"chroma_subsample":2,"separate_chroma_quality":false,"chroma_quality":75} --output-dir "${path.dirname(
img
)}" "${img}"`
);
break;
}
});

View File

@@ -1,26 +1,24 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1">
<link <link href="/404/bootstrap.min.css" rel="stylesheet" type="text/css"
href="/404/bootstrap.min.css" integrity="sha384-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l">
rel="stylesheet" <link href="/404/font-awesome.min.css" rel="stylesheet" type="text/css"
type="text/css" integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw==">
integrity="sha384-hA/ESrxp2b05ywLtD9YwM6m+pNyLRY4+ruk6dWK00SM4k6SQs0bfrITJVSf6uZyH" <link href="/404/styles.css" rel="stylesheet" type="text/css">
/>
<link href="/404/styles.css" rel="stylesheet" type="text/css" />
<link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/images/icons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png" /> <link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png">
<link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#175DDC" /> <link rel="mask-icon" href="/images/icons/safari-pinned-tab.svg" color="#175DDC">
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json">
<title>Page not found!</title> <title>Page not found!</title>
<meta name="description" content="404 Page Not Found" /> <meta name="description" content="404 Page Not Found">
</head> </head>
<body> <body>
@@ -28,7 +26,8 @@
<div class="container inner banner"> <div class="container inner banner">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col brand"> <div class="col brand">
<i class="bwi bwi-shield"></i>&nbsp; <strong>bit</strong>warden <i class="fa fa-shield"></i>&nbsp;
<strong>bit</strong>warden</span>
</div> </div>
</div> </div>
</div> </div>
@@ -38,15 +37,14 @@
<p>Sorry, but the page you were looking for could not be found.</p> <p>Sorry, but the page you were looking for could not be found.</p>
<p> <p>
<a href="/"> <a href="/">
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%" /> <img src="/images/404.png" class="img-fluid" alt="404 image" width="80%"/>
</a> </a>
</p> </p>
<p> <p>You can <a href="/">return to the web vault</a>, check our <a href="https://status.bitwarden.com/">status page</a>
You can <a href="/">return to the web vault</a>, check our or <a href="https://bitwarden.com/contact/">contact us</a>.</p>
<a href="https://status.bitwarden.com/">status page</a> or </div>
<a href="https://bitwarden.com/contact/">contact us</a>. <div class="container footer text-muted content">
</p> © Copyright 2021 Bitwarden, Inc.
</div> </div>
<div class="container footer text-muted content">© Copyright 2022 Bitwarden, Inc.</div>
</body> </body>
</html> </html>

4
src/404/font-awesome.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,90 +1,88 @@
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
src: url(../fonts/Open_Sans-italic-300.woff) format("woff"); src: url(../fonts/Open_Sans-italic-300.woff) format('woff');
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
src: url(../fonts/Open_Sans-italic-400.woff) format("woff"); src: url(../fonts/Open_Sans-italic-400.woff) format('woff');
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
src: url(../fonts/Open_Sans-italic-600.woff) format("woff"); src: url(../fonts/Open_Sans-italic-600.woff) format('woff');
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
src: url(../fonts/Open_Sans-italic-700.woff) format("woff"); src: url(../fonts/Open_Sans-italic-700.woff) format('woff');
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
font-style: italic; font-style: italic;
font-weight: 800; font-weight: 800;
src: url(../fonts/Open_Sans-italic-800.woff) format("woff"); src: url(../fonts/Open_Sans-italic-800.woff) format('woff');
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: url(../fonts/Open_Sans-normal-300.woff) format("woff"); src: url(../fonts/Open_Sans-normal-300.woff) format('woff');
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url(../fonts/Open_Sans-normal-400.woff) format("woff"); src: url(../fonts/Open_Sans-normal-400.woff) format('woff');
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: url(../fonts/Open_Sans-normal-600.woff) format("woff"); src: url(../fonts/Open_Sans-normal-600.woff) format('woff');
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: url(../fonts/Open_Sans-normal-700.woff) format("woff"); src: url(../fonts/Open_Sans-normal-700.woff) format('woff');
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
src: url(../fonts/Open_Sans-normal-800.woff) format("woff"); src: url(../fonts/Open_Sans-normal-800.woff) format('woff');
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
body { body {
font-family: "Open Sans"; font-family: 'Open Sans';
} }
html, html, body, .row {
body,
.row {
height: 100%; height: 100%;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
@@ -100,11 +98,11 @@ h2 {
font-size: 23px; font-size: 23px;
line-height: 25px; line-height: 25px;
color: #fff; color: #fff;
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif;
} }
.banner { .banner {
background-color: #175ddc; background-color: #175DDC;
height: 56px; height: 56px;
} }
@@ -119,33 +117,3 @@ h2 {
padding: 40px 0 40px 0; padding: 40px 0 40px 0;
border-top: 1px solid #dee2e6; border-top: 1px solid #dee2e6;
} }
/* Bitwarden icons, manually copied */
@font-face {
font-family: "bwi-font";
src: url(../images/bwi-font.svg) format("svg"), url(../fonts/bwi-font.ttf) format("truetype"),
url(../fonts/bwi-font.woff) format("woff"), url(../fonts/bwi-font.woff2) format("woff2");
font-weight: normal;
font-style: normal;
font-display: block;
}
.bwi {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: "bwi-font" !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
display: inline-block;
/* Better Font Rendering */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.bwi-shield:before {
content: "\e932";
}

View File

@@ -1,9 +0,0 @@
import { StateService as BaseStateService } from "jslib-common/abstractions/state.service";
import { StorageOptions } from "jslib-common/models/domain/storageOptions";
import { Account } from "src/models/account";
export abstract class StateService extends BaseStateService<Account> {
getRememberEmail: (options?: StorageOptions) => Promise<boolean>;
setRememberEmail: (value: boolean, options?: StorageOptions) => Promise<void>;
}

View File

@@ -1,41 +1,30 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading"> <div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div> <div>
<img class="mb-4 logo logo-themed" alt="Bitwarden" /> <img class="mb-4 logo logo-themed" alt="Bitwarden">
<p class="text-center"> <p class="text-center">
<i <i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin bwi-2x text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p> </p>
</div> </div>
</div> </div>
<div class="container" *ngIf="!loading && !authed"> <div class="container" *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="lead text-center mb-4">{{ "emergencyAccess" | i18n }}</p> <p class="lead text-center mb-4">{{'emergencyAccess' | i18n}}</p>
<div class="card d-block"> <div class="card d-block">
<div class="card-body"> <div class="card-body">
<p class="text-center"> <p class="text-center">
{{ name }} {{name}}
</p> </p>
<p>{{ "acceptEmergencyAccess" | i18n }}</p> <p>{{'acceptEmergencyAccess' | i18n}}</p>
<hr /> <hr>
<div class="d-flex"> <div class="d-flex">
<a <a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
routerLink="/login" {{'logIn' | i18n}}
[queryParams]="{ email: email }"
class="btn btn-primary btn-block"
>
{{ "logIn" | i18n }}
</a> </a>
<a <a routerLink="/register" [queryParams]="{email: email}"
routerLink="/register" class="btn btn-primary btn-block ml-2 mt-0">
[queryParams]="{ email: email }" {{'createAccount' | i18n}}
class="btn btn-primary btn-block ml-2 mt-0"
>
{{ "createAccount" | i18n }}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,34 +1,38 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute, Router } from "@angular/router"; import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from "jslib-common/abstractions/api.service"; import {
import { I18nService } from "jslib-common/abstractions/i18n.service"; Toast,
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; ToasterService,
import { StateService } from "jslib-common/abstractions/state.service"; } from 'angular2-toaster';
import { EmergencyAccessAcceptRequest } from "jslib-common/models/request/emergencyAccessAcceptRequest";
import { BaseAcceptComponent } from "../common/base.accept.component"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { EmergencyAccessAcceptRequest } from 'jslib-common/models/request/emergencyAccessAcceptRequest';
import { BaseAcceptComponent } from '../common/base.accept.component';
@Component({ @Component({
selector: "app-accept-emergency", selector: 'app-accept-emergency',
templateUrl: "accept-emergency.component.html", templateUrl: 'accept-emergency.component.html',
}) })
export class AcceptEmergencyComponent extends BaseAcceptComponent { export class AcceptEmergencyComponent extends BaseAcceptComponent {
name: string; name: string;
protected requiredParameters: string[] = ["id", "name", "email", "token"]; protected requiredParameters: string[] = ['id', 'name', 'email', 'token'];
protected failedShortMessage = "emergencyInviteAcceptFailedShort"; protected failedShortMessage = 'emergencyInviteAcceptFailedShort';
protected failedMessage = "emergencyInviteAcceptFailed"; protected failedMessage = 'emergencyInviteAcceptFailed';
constructor( constructor(router: Router, toasterService: ToasterService,
router: Router, i18nService: I18nService, route: ActivatedRoute,
platformUtilsService: PlatformUtilsService, private apiService: ApiService, userService: UserService,
i18nService: I18nService, stateService: StateService) {
route: ActivatedRoute, super(router, toasterService, i18nService, route, userService, stateService);
private apiService: ApiService,
stateService: StateService
) {
super(router, platformUtilsService, i18nService, route, stateService);
} }
async authedHandler(qParams: any): Promise<void> { async authedHandler(qParams: any): Promise<void> {
@@ -36,20 +40,21 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent {
request.token = qParams.token; request.token = qParams.token;
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request); this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
await this.actionPromise; await this.actionPromise;
this.platformUtilService.showToast( const toast: Toast = {
"success", type: 'success',
this.i18nService.t("inviteAccepted"), title: this.i18nService.t('inviteAccepted'),
this.i18nService.t("emergencyInviteAcceptedDesc"), body: this.i18nService.t('emergencyInviteAcceptedDesc'),
{ timeout: 10000 } timeout: 10000,
); };
this.router.navigate(["/vault"]); this.toasterService.popAsync(toast);
this.router.navigate(['/vault']);
} }
async unauthedHandler(qParams: any): Promise<void> { async unauthedHandler(qParams: any): Promise<void> {
this.name = qParams.name; this.name = qParams.name;
if (this.name != null) { if (this.name != null) {
// Fix URL encoding of space issue with Angular // Fix URL encoding of space issue with Angular
this.name = this.name.replace(/\+/g, " "); this.name = this.name.replace(/\+/g, ' ');
} }
} }
} }

View File

@@ -1,42 +1,31 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading"> <div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div> <div>
<img src="../../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden" /> <img src="../../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden">
<p class="text-center"> <p class="text-center">
<i <i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
class="bwi bwi-spinner bwi-spin bwi-2x text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p> </p>
</div> </div>
</div> </div>
<div class="container" *ngIf="!loading && !authed"> <div class="container" *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="lead text-center mb-4">{{ "joinOrganization" | i18n }}</p> <p class="lead text-center mb-4">{{'joinOrganization' | i18n}}</p>
<div class="card d-block"> <div class="card d-block">
<div class="card-body"> <div class="card-body">
<p class="text-center"> <p class="text-center">
{{ orgName }} {{orgName}}
<strong class="d-block mt-2">{{ email }}</strong> <strong class="d-block mt-2">{{email}}</strong>
</p> </p>
<p>{{ "joinOrganizationDesc" | i18n }}</p> <p>{{'joinOrganizationDesc' | i18n}}</p>
<hr /> <hr>
<div class="d-flex"> <div class="d-flex">
<a <a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
routerLink="/login" {{'logIn' | i18n}}
[queryParams]="{ email: email }"
class="btn btn-primary btn-block"
>
{{ "logIn" | i18n }}
</a> </a>
<a <a routerLink="/register" [queryParams]="{email: email}"
routerLink="/register" class="btn btn-primary btn-block ml-2 mt-0">
[queryParams]="{ email: email }" {{'createAccount' | i18n}}
class="btn btn-primary btn-block ml-2 mt-0"
>
{{ "createAccount" | i18n }}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,56 +1,57 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute, Router } from "@angular/router"; import {
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from "jslib-common/abstractions/api.service"; import {
import { CryptoService } from "jslib-common/abstractions/crypto.service"; Toast,
import { I18nService } from "jslib-common/abstractions/i18n.service"; ToasterService,
import { LogService } from "jslib-common/abstractions/log.service"; } from 'angular2-toaster';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { Utils } from "jslib-common/misc/utils";
import { Policy } from "jslib-common/models/domain/policy";
import { OrganizationUserAcceptRequest } from "jslib-common/models/request/organizationUserAcceptRequest";
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
import { BaseAcceptComponent } from "../common/base.accept.component"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { OrganizationUserAcceptRequest } from 'jslib-common/models/request/organizationUserAcceptRequest';
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest';
import { Utils } from 'jslib-common/misc/utils';
import { Policy } from 'jslib-common/models/domain/policy';
import { BaseAcceptComponent } from '../common/base.accept.component';
@Component({ @Component({
selector: "app-accept-organization", selector: 'app-accept-organization',
templateUrl: "accept-organization.component.html", templateUrl: 'accept-organization.component.html',
}) })
export class AcceptOrganizationComponent extends BaseAcceptComponent { export class AcceptOrganizationComponent extends BaseAcceptComponent {
orgName: string; orgName: string;
protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"]; protected requiredParameters: string[] = ['organizationId', 'organizationUserId', 'token'];
constructor( constructor(router: Router, toasterService: ToasterService,
router: Router, i18nService: I18nService, route: ActivatedRoute,
platformUtilsService: PlatformUtilsService, private apiService: ApiService, userService: UserService,
i18nService: I18nService, stateService: StateService, private cryptoService: CryptoService,
route: ActivatedRoute, private policyService: PolicyService, private logService: LogService) {
private apiService: ApiService, super(router, toasterService, i18nService, route, userService, stateService);
stateService: StateService,
private cryptoService: CryptoService,
private policyService: PolicyService,
private logService: LogService
) {
super(router, platformUtilsService, i18nService, route, stateService);
} }
async authedHandler(qParams: any): Promise<void> { async authedHandler(qParams: any): Promise<void> {
const request = new OrganizationUserAcceptRequest(); const request = new OrganizationUserAcceptRequest();
request.token = qParams.token; request.token = qParams.token;
if (await this.performResetPasswordAutoEnroll(qParams)) { if (await this.performResetPasswordAutoEnroll(qParams)) {
this.actionPromise = this.apiService this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
.postOrganizationUserAccept(qParams.organizationId, qParams.organizationUserId, request) qParams.organizationUserId, request).then(() => {
.then(() => {
// Retrieve Public Key // Retrieve Public Key
return this.apiService.getOrganizationKeys(qParams.organizationId); return this.apiService.getOrganizationKeys(qParams.organizationId);
}) }).then(async response => {
.then(async (response) => {
if (response == null) { if (response == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError")); throw new Error(this.i18nService.t('resetPasswordOrgKeysError'));
} }
const publicKey = Utils.fromB64ToArray(response.publicKey); const publicKey = Utils.fromB64ToArray(response.publicKey);
@@ -63,60 +64,50 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent {
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.resetPasswordKey = encryptedKey.encryptedString; resetRequest.resetPasswordKey = encryptedKey.encryptedString;
return this.apiService.putOrganizationUserResetPasswordEnrollment( // Get User Id
qParams.organizationId, const userId = await this.userService.getUserId();
await this.stateService.getUserId(),
resetRequest return this.apiService.putOrganizationUserResetPasswordEnrollment(qParams.organizationId, userId, resetRequest);
);
}); });
} else { } else {
this.actionPromise = this.apiService.postOrganizationUserAccept( this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
qParams.organizationId, qParams.organizationUserId, request);
qParams.organizationUserId,
request
);
} }
await this.actionPromise; await this.actionPromise;
this.platformUtilService.showToast( const toast: Toast = {
"success", type: 'success',
this.i18nService.t("inviteAccepted"), title: this.i18nService.t('inviteAccepted'),
this.i18nService.t("inviteAcceptedDesc"), body: this.i18nService.t('inviteAcceptedDesc'),
{ timeout: 10000 } timeout: 10000,
); };
this.toasterService.popAsync(toast);
await this.stateService.setOrganizationInvitation(null); await this.stateService.remove('orgInvitation');
this.router.navigate(["/vault"]); this.router.navigate(['/vault']);
} }
async unauthedHandler(qParams: any): Promise<void> { async unauthedHandler(qParams: any): Promise<void> {
this.orgName = qParams.organizationName; this.orgName = qParams.organizationName;
if (this.orgName != null) { if (this.orgName != null) {
// Fix URL encoding of space issue with Angular // Fix URL encoding of space issue with Angular
this.orgName = this.orgName.replace(/\+/g, " "); this.orgName = this.orgName.replace(/\+/g, ' ');
} }
await this.stateService.setOrganizationInvitation(qParams); await this.stateService.save('orgInvitation', qParams);
} }
private async performResetPasswordAutoEnroll(qParams: any): Promise<boolean> { private async performResetPasswordAutoEnroll(qParams: any): Promise<boolean> {
let policyList: Policy[] = null; let policyList: Policy[] = null;
try { try {
const policies = await this.apiService.getPoliciesByToken( const policies = await this.apiService.getPoliciesByToken(qParams.organizationId, qParams.token,
qParams.organizationId, qParams.email, qParams.organizationUserId);
qParams.token,
qParams.email,
qParams.organizationUserId
);
policyList = this.policyService.mapPoliciesFromToken(policies); policyList = this.policyService.mapPoliciesFromToken(policies);
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
if (policyList != null) { if (policyList != null) {
const result = this.policyService.getResetPasswordPolicyOptions( const result = this.policyService.getResetPasswordPolicyOptions(policyList, qParams.organizationId);
policyList,
qParams.organizationId
);
// Return true if policy enabled and auto-enroll enabled // Return true if policy enabled and auto-enroll enabled
return result[1] && result[0].autoEnrollEnabled; return result[1] && result[0].autoEnrollEnabled;
} }

View File

@@ -1,40 +1,23 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="lead text-center mb-4">{{ "passwordHint" | i18n }}</p> <p class="lead text-center mb-4">{{'passwordHint' | i18n}}</p>
<div class="card d-block"> <div class="card d-block">
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="email">{{ "emailAddress" | i18n }}</label> <label for="email">{{'emailAddress' | i18n}}</label>
<input <input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
id="email" appAutofocus inputmode="email" appInputVerbatim="false">
class="form-control" <small class="form-text text-muted">{{'enterEmailToGetHint' | i18n}}</small>
type="text"
name="Email"
[(ngModel)]="email"
required
appAutofocus
inputmode="email"
appInputVerbatim="false"
/>
<small class="form-text text-muted">{{ "enterEmailToGetHint" | i18n }}</small>
</div> </div>
<hr /> <hr>
<div class="d-flex"> <div class="d-flex">
<button <button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
type="submit" <span [hidden]="form.loading">{{'submit' | i18n}}</span>
class="btn btn-primary btn-block btn-submit" <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
[disabled]="form.loading"
>
<span [hidden]="form.loading">{{ "submit" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button> </button>
<a routerLink="/login" class="btn btn-outline-secondary btn-block ml-2 mt-0"> <a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }} {{'cancel' | i18n}}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,24 +1,21 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { Router } from "@angular/router"; import { Router } from '@angular/router';
import { HintComponent as BaseHintComponent } from "jslib-angular/components/hint.component"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { ApiService } from "jslib-common/abstractions/api.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { LogService } from 'jslib-common/abstractions/log.service';
import { LogService } from "jslib-common/abstractions/log.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { HintComponent as BaseHintComponent } from 'jslib-angular/components/hint.component';
@Component({ @Component({
selector: "app-hint", selector: 'app-hint',
templateUrl: "hint.component.html", templateUrl: 'hint.component.html',
}) })
export class HintComponent extends BaseHintComponent { export class HintComponent extends BaseHintComponent {
constructor( constructor(router: Router, i18nService: I18nService,
router: Router, apiService: ApiService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService, logService: LogService) {
apiService: ApiService,
platformUtilsService: PlatformUtilsService,
logService: LogService
) {
super(router, i18nService, apiService, platformUtilsService, logService); super(router, i18nService, apiService, platformUtilsService, logService);
} }
} }

View File

@@ -2,61 +2,37 @@
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="text-center mb-4"> <p class="text-center mb-4">
<i class="bwi bwi-lock bwi-4x text-muted" aria-hidden="true"></i> <i class="fa fa-lock fa-4x text-muted" aria-hidden="true"></i>
</p> </p>
<p class="lead text-center mx-4 mb-4">{{ "yourVaultIsLocked" | i18n }}</p> <p class="lead text-center mx-4 mb-4">{{'yourVaultIsLocked' | i18n}}</p>
<div class="card d-block"> <div class="card d-block">
<div class="card-body"> <div class="card-body">
<div class="form-group"> <div class="form-group">
<label for="masterPassword">{{ "masterPass" | i18n }}</label> <label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex"> <div class="d-flex">
<input <input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
id="masterPassword" name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}" required appAutofocus appInputVerbatim>
name="MasterPassword" <button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
class="text-monospace form-control" (click)="togglePassword()">
[(ngModel)]="masterPassword" <i class="fa fa-lg" aria-hidden="true"
required [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
appAutofocus
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button> </button>
</div> </div>
<small class="text-muted form-text"> <small class="text-muted form-text">
{{ "loggedInAsEmailOn" | i18n: email:webVaultHostname }} {{'loggedInAsEmailOn' | i18n : email : webVaultHostname}}
</small> </small>
</div> </div>
<hr /> <hr>
<div class="d-flex"> <div class="d-flex">
<button <button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
type="submit" <span>
class="btn btn-primary btn-block btn-submit" <i class="fa fa-unlock-alt" aria-hidden="true"></i> {{'unlock' | i18n}}
[disabled]="form.loading" </span>
> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span> <i class="bwi bwi-unlock" aria-hidden="true"></i> {{ "unlock" | i18n }} </span>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button> </button>
<button <button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
type="button" {{'logOut' | i18n}}
class="btn btn-outline-secondary btn-block ml-2 mt-0"
(click)="logOut()"
>
{{ "logOut" | i18n }}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,64 +1,48 @@
import { Component, NgZone } from "@angular/core"; import { Component } from '@angular/core';
import { Router } from "@angular/router"; import { Router } from '@angular/router';
import { LockComponent as BaseLockComponent } from "jslib-angular/components/lock.component"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { ApiService } from "jslib-common/abstractions/api.service"; import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { EnvironmentService } from "jslib-common/abstractions/environment.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service';
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; import { LogService } from 'jslib-common/abstractions/log.service';
import { LogService } from "jslib-common/abstractions/log.service"; import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { MessagingService } from "jslib-common/abstractions/messaging.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { StateService } from 'jslib-common/abstractions/state.service';
import { StateService } from "jslib-common/abstractions/state.service"; import { StorageService } from 'jslib-common/abstractions/storage.service';
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service';
import { RouterService } from "../services/router.service"; import { RouterService } from '../services/router.service';
import { LockComponent as BaseLockComponent } from 'jslib-angular/components/lock.component';
@Component({ @Component({
selector: "app-lock", selector: 'app-lock',
templateUrl: "lock.component.html", templateUrl: 'lock.component.html',
}) })
export class LockComponent extends BaseLockComponent { export class LockComponent extends BaseLockComponent {
constructor( constructor(router: Router, i18nService: I18nService,
router: Router, platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
i18nService: I18nService, userService: UserService, cryptoService: CryptoService,
platformUtilsService: PlatformUtilsService, storageService: StorageService, vaultTimeoutService: VaultTimeoutService,
messagingService: MessagingService, environmentService: EnvironmentService, private routerService: RouterService,
cryptoService: CryptoService, stateService: StateService, apiService: ApiService, logService: LogService,
vaultTimeoutService: VaultTimeoutService, keyConnectorService: KeyConnectorService) {
environmentService: EnvironmentService, super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
private routerService: RouterService, storageService, vaultTimeoutService, environmentService, stateService, apiService, logService,
stateService: StateService, keyConnectorService);
apiService: ApiService,
logService: LogService,
keyConnectorService: KeyConnectorService,
ngZone: NgZone
) {
super(
router,
i18nService,
platformUtilsService,
messagingService,
cryptoService,
vaultTimeoutService,
environmentService,
stateService,
apiService,
logService,
keyConnectorService,
ngZone
);
} }
async ngOnInit() { async ngOnInit() {
await super.ngOnInit(); await super.ngOnInit();
this.onSuccessfulSubmit = async () => { this.onSuccessfulSubmit = () => {
const previousUrl = this.routerService.getPreviousUrl(); const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl && previousUrl !== "/" && previousUrl.indexOf("lock") === -1) { if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {
this.successRoute = previousUrl; this.successRoute = previousUrl;
} }
this.router.navigateByUrl(this.successRoute); this.router.navigate([this.successRoute]);
}; };
} }
} }

View File

@@ -1,98 +1,57 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<img class="mb-2 logo logo-themed" alt="Bitwarden" /> <img class="mb-2 logo logo-themed" alt="Bitwarden">
<p class="lead text-center mx-4 mb-4">{{ "loginOrCreateNewAccount" | i18n }}</p> <p class="lead text-center mx-4 mb-4">{{'loginOrCreateNewAccount' | i18n}}</p>
<div class="card d-block"> <div class="card d-block">
<div class="card-body"> <div class="card-body">
<app-callout <app-callout type="warning" title="{{'resetPasswordPolicyAutoEnroll' | i18n}}"
type="warning" *ngIf="showResetPasswordAutoEnrollWarning">
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}" {{'resetPasswordAutoEnrollInviteWarning' | i18n}}
*ngIf="showResetPasswordAutoEnrollWarning"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</app-callout> </app-callout>
<div class="form-group"> <div class="form-group">
<label for="email">{{ "emailAddress" | i18n }}</label> <label for="email">{{'emailAddress' | i18n}}</label>
<input <input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
id="email" inputmode="email" appInputVerbatim="false">
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
inputmode="email"
appInputVerbatim="false"
/>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="masterPassword">{{ "masterPass" | i18n }}</label> <label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex"> <div class="d-flex">
<input <input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
id="masterPassword" name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}" required appInputVerbatim>
name="MasterPassword" <button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
class="text-monospace form-control" (click)="togglePassword()">
[(ngModel)]="masterPassword" <i class="fa fa-lg" aria-hidden="true"
required [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button> </button>
</div> </div>
<small class="form-text"> <small class="form-text">
<a routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</a> <a routerLink="/hint">{{'getMasterPasswordHint' | i18n}}</a>
</small> </small>
</div> </div>
<div class="form-check mb-3"> <div class="form-check mb-3">
<input <input type="checkbox" class="form-check-input" id="rememberEmail" name="RememberEmail"
type="checkbox" [(ngModel)]="rememberEmail">
class="form-check-input" <label class="form-check-label" for="rememberEmail">{{'rememberEmail' | i18n}}</label>
id="rememberEmail"
name="RememberEmail"
[(ngModel)]="rememberEmail"
/>
<label class="form-check-label" for="rememberEmail">{{ "rememberEmail" | i18n }}</label>
</div> </div>
<div class="mb-n3" [hidden]="!showCaptcha()"> <div class="mb-n3" [hidden]="!showCaptcha()"><iframe id="hcaptcha_iframe" height="80"></iframe></div>
<iframe id="hcaptcha_iframe" height="80"></iframe> <hr>
</div>
<hr />
<div class="d-flex"> <div class="d-flex">
<button <button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
type="submit" <span>
class="btn btn-primary btn-block btn-submit" <i class="fa fa-sign-in" aria-hidden="true"></i> {{'logIn' | i18n}}
[disabled]="form.loading" </span>
> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span> <i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "logIn" | i18n }} </span>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button> </button>
<a <a routerLink="/register" [queryParams]="{email: email}"
routerLink="/register" class="btn btn-outline-secondary btn-block ml-2 mt-0">
[queryParams]="{ email: email }" <i class="fa fa-pencil-square-o" aria-hidden="true"></i> {{'createAccount' | i18n}}
class="btn btn-outline-secondary btn-block ml-2 mt-0"
>
<i class="bwi bwi-pencil-square" aria-hidden="true"></i>
{{ "createAccount" | i18n }}
</a> </a>
</div> </div>
<div class="d-flex"> <div class="d-flex">
<a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2"> <a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2">
<i class="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }} <i class="fa fa-bank" aria-hidden="true"></i> {{'enterpriseSingleSignOn' | i18n}}
</a> </a>
</div> </div>
</div> </div>

View File

@@ -1,179 +1,98 @@
import { Component, NgZone } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute, Router } from "@angular/router"; import {
import { first } from "rxjs/operators"; ActivatedRoute,
Router,
} from '@angular/router';
import { LoginComponent as BaseLoginComponent } from "jslib-angular/components/login.component"; import { first } from 'rxjs/operators';
import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from "jslib-common/abstractions/policy.service";
import { PolicyData } from "jslib-common/models/data/policyData";
import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPasswordPolicyOptions";
import { Policy } from "jslib-common/models/domain/policy";
import { ListResponse } from "jslib-common/models/response/listResponse";
import { PolicyResponse } from "jslib-common/models/response/policyResponse";
import { StateService } from "../../abstractions/state.service"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { RouterService } from "../services/router.service"; import { AuthService } from 'jslib-common/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { LoginComponent as BaseLoginComponent } from 'jslib-angular/components/login.component';
import { Policy } from 'jslib-common/models/domain/policy';
@Component({ @Component({
selector: "app-login", selector: 'app-login',
templateUrl: "login.component.html", templateUrl: 'login.component.html',
}) })
export class LoginComponent extends BaseLoginComponent { export class LoginComponent extends BaseLoginComponent {
showResetPasswordAutoEnrollWarning = false;
enforcedPasswordPolicyOptions: MasterPasswordPolicyOptions;
policies: ListResponse<PolicyResponse>;
constructor( showResetPasswordAutoEnrollWarning = false;
authService: AuthService,
router: Router, constructor(authService: AuthService, router: Router,
i18nService: I18nService, i18nService: I18nService, private route: ActivatedRoute,
private route: ActivatedRoute, storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
environmentService: EnvironmentService, passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService,
passwordGenerationService: PasswordGenerationService, private apiService: ApiService, private policyService: PolicyService, logService: LogService) {
cryptoFunctionService: CryptoFunctionService, super(authService, router,
private apiService: ApiService, platformUtilsService, i18nService,
private policyService: PolicyService, stateService, environmentService,
logService: LogService, passwordGenerationService, cryptoFunctionService,
ngZone: NgZone, storageService, logService);
protected stateService: StateService,
private messagingService: MessagingService,
private routerService: RouterService
) {
super(
authService,
router,
platformUtilsService,
i18nService,
stateService,
environmentService,
passwordGenerationService,
cryptoFunctionService,
logService,
ngZone
);
this.onSuccessfulLogin = async () => {
this.messagingService.send("setFullWidth");
};
this.onSuccessfulLoginNavigate = this.goAfterLogIn; this.onSuccessfulLoginNavigate = this.goAfterLogIn;
} }
async ngOnInit() { async ngOnInit() {
this.route.queryParams.pipe(first()).subscribe(async (qParams) => { this.route.queryParams.pipe(first()).subscribe(async qParams => {
if (qParams.email != null && qParams.email.indexOf("@") > -1) { if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.email = qParams.email; this.email = qParams.email;
} }
if (qParams.premium != null) { if (qParams.premium != null) {
this.routerService.setPreviousUrl("/settings/premium"); this.stateService.save('loginRedirect', { route: '/settings/premium' });
} else if (qParams.org != null) { } else if (qParams.org != null) {
const route = this.router.createUrlTree(["create-organization"], { this.stateService.save('loginRedirect',
queryParams: { plan: qParams.org }, { route: '/settings/create-organization', qParams: { plan: qParams.org } });
});
this.routerService.setPreviousUrl(route.toString());
} }
// Are they coming from an email for sponsoring a families organization // Are they coming from an email for sponsoring a families organization
if (qParams.sponsorshipToken != null) { if (qParams.sponsorshipToken != null) {
const route = this.router.createUrlTree(["setup/families-for-enterprise"], { // After logging in redirect them to setup the families sponsorship
queryParams: { token: qParams.sponsorshipToken }, this.stateService.save('loginRedirect', {
route: '/setup/families-for-enterprise',
qParams: { token: qParams.sponsorshipToken },
}); });
this.routerService.setPreviousUrl(route.toString());
} }
await super.ngOnInit(); await super.ngOnInit();
this.rememberEmail = await this.stateService.getRememberEmail();
}); });
const invite = await this.stateService.getOrganizationInvitation(); const invite = await this.stateService.get<any>('orgInvitation');
if (invite != null) { if (invite != null) {
let policyList: Policy[] = null; let policyList: Policy[] = null;
try { try {
this.policies = await this.apiService.getPoliciesByToken( const policies = await this.apiService.getPoliciesByToken(invite.organizationId, invite.token,
invite.organizationId, invite.email, invite.organizationUserId);
invite.token, policyList = this.policyService.mapPoliciesFromToken(policies);
invite.email,
invite.organizationUserId
);
policyList = this.policyService.mapPoliciesFromToken(this.policies);
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
if (policyList != null) { if (policyList != null) {
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions( const result = this.policyService.getResetPasswordPolicyOptions(policyList, invite.organizationId);
policyList,
invite.organizationId
);
// Set to true if policy enabled and auto-enroll enabled // Set to true if policy enabled and auto-enroll enabled
this.showResetPasswordAutoEnrollWarning = this.showResetPasswordAutoEnrollWarning = result[1] && result[0].autoEnrollEnabled;
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
this.enforcedPasswordPolicyOptions =
await this.policyService.getMasterPasswordPolicyOptions(policyList);
} }
} }
} }
async goAfterLogIn() { async goAfterLogIn() {
// Check master password against policy const loginRedirect = await this.stateService.get<any>('loginRedirect');
if (this.enforcedPasswordPolicyOptions != null) { if (loginRedirect != null) {
const strengthResult = this.passwordGenerationService.passwordStrength( this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
this.masterPassword, await this.stateService.remove('loginRedirect');
this.getPasswordStrengthUserInput()
);
const masterPasswordScore = strengthResult == null ? null : strengthResult.score;
// If invalid, save policies and require update
if (
!this.policyService.evaluateMasterPassword(
masterPasswordScore,
this.masterPassword,
this.enforcedPasswordPolicyOptions
)
) {
const policiesData: { [id: string]: PolicyData } = {};
this.policies.data.map((p) => (policiesData[p.id] = new PolicyData(p)));
await this.policyService.replace(policiesData);
this.router.navigate(["update-password"]);
return;
}
}
const previousUrl = this.routerService.getPreviousUrl();
if (previousUrl) {
this.router.navigateByUrl(previousUrl);
} else { } else {
this.router.navigate([this.successRoute]); this.router.navigate([this.successRoute]);
} }
} }
async submit() {
await this.stateService.setRememberEmail(this.rememberEmail);
if (!this.rememberEmail) {
await this.stateService.setRememberedEmail(null);
}
await super.submit();
}
private getPasswordStrengthUserInput() {
let userInput: string[] = [];
const atPosition = this.email.indexOf("@");
if (atPosition > -1) {
userInput = userInput.concat(
this.email
.substr(0, atPosition)
.trim()
.toLowerCase()
.split(/[^A-Za-z0-9]/)
);
}
return userInput;
}
} }

Some files were not shown because too many files have changed in this diff Show More