1
0
mirror of https://github.com/bitwarden/web synced 2025-12-12 06:13:28 +00:00

Compare commits

..

27 Commits

Author SHA1 Message Date
Vince Grassia
3f5f3dc5c8 Bump version 2021-11-10 14:06:14 -05:00
Justin Baur
c01d95f870 Update payment info (#1274)
* Added manual routing

* Add additional copy for free trial

* Revert

* Fix formatting

* Switch text to be on the top of the payment info

* Update to put text at top of the screen

(cherry picked from commit b164a39abc)
2021-11-10 14:04:16 -05:00
Matt Gibson
7b58b66e17 Fix repeat ng insert on safari (#1270)
(cherry picked from commit cf5823fe71)
2021-11-01 17:15:11 -04:00
Matt Gibson
95b9e6fb24 bump version 2021-11-01 15:34:57 -04:00
Matt Gibson
52c057555c Show upgrade plan button for free orgs. (#1269)
* Show upgrade plan button for free orgs.

* Add families plan callout for subscription upgrade

(cherry picked from commit bb0b5f2d87)
2021-11-01 15:32:44 -04:00
Joseph Flinn
a6274fa56e Adding fixes from last night's release (#1263)
(cherry picked from commit 523b18156c)
2021-10-27 13:09:50 -07:00
Matt Gibson
c1ede75a98 Update jslib for permissions changes 2021-10-27 14:16:54 -04:00
Matt Gibson
16cf1c4a47 2.24.1 2021-10-27 11:04:22 -04:00
Matt Gibson
477bb997dc Null Check subscription prior to use (#1264)
(cherry picked from commit 383c29c761)
2021-10-27 11:04:10 -04:00
Joseph Flinn
9e3d45da6c changing how we checkout the new release branch 2021-10-26 20:48:03 -07:00
github-actions[bot]
8892553e30 Autosync the updated translations (#1259)
Co-authored-by: github-actions <>
(cherry picked from commit a6a34788a8)
2021-10-26 15:10:35 -07:00
Joseph Flinn
95ff586a47 enabling the new release branch to push its docker images (#1258)
(cherry picked from commit 381ec7af67)
2021-10-26 14:21:58 -07:00
Joseph Flinn
342e09da9e Version Bump 2.24.0
(cherry picked from commit c589728506)
2021-10-26 13:48:47 -07:00
Joseph Flinn
32cecf6086 Change release branch constraints (#1248)
* updating the release branch constraints

* updating the self host docker image build and release with the new release branch

* renaming the release job for selfhost docker release

* removing unneeded line

* removing the master branch release ci code execution

* updating some verbiage

(cherry picked from commit 618f950cae)
2021-10-22 09:31:39 -07:00
Matt Gibson
b122d24350 Update jslib 2021-10-20 17:26:50 -04:00
Matt Gibson
a620a3f84c Limit collection actions presented to permitted (#1247)
* Limit collection actions presented to permitted

* Revert useless move

* Limit vault view to editable ciphers and collections

* Update jslib

* PR review

(cherry picked from commit 9dd859af7a)
2021-10-20 17:21:29 -04:00
Thomas Rittson
3d30495ffb Fix btn-link colors in dark mode (#1246) 2021-10-20 07:50:25 +10:00
Kyle Spearrin
bb03595d02 New Crowdin updates (#1242)
* New translations messages.json (Romanian)

* New translations messages.json (Korean)

* New translations messages.json (Vietnamese)

* New translations messages.json (Chinese Traditional)

* New translations messages.json (Chinese Simplified)

* New translations messages.json (Ukrainian)

* New translations messages.json (Turkish)

* New translations messages.json (French)

* New translations messages.json (Serbian (Cyrillic))

* New translations messages.json (Slovenian)

* New translations messages.json (Slovak)

* New translations messages.json (Russian)

* New translations messages.json (Portuguese)

* New translations messages.json (Polish)

* New translations messages.json (Dutch)

* New translations messages.json (Swedish)

* New translations messages.json (Japanese)

* New translations messages.json (Czech)

* New translations messages.json (Italian)

* New translations messages.json (Spanish)

* New translations messages.json (Afrikaans)

* New translations messages.json (Bulgarian)

* New translations messages.json (Catalan)

* New translations messages.json (Belarusian)

* New translations messages.json (Danish)

* New translations messages.json (German)

* New translations messages.json (Greek)

* New translations messages.json (Finnish)

* New translations messages.json (Hebrew)

* New translations messages.json (Hungarian)

* New translations messages.json (English, United Kingdom)

* New translations messages.json (Serbian (Latin))

* New translations messages.json (Norwegian Bokmal)

* New translations messages.json (Kannada)

* New translations messages.json (Sinhala)

* New translations messages.json (Malayalam)

* New translations messages.json (Filipino)

* New translations messages.json (Esperanto)

* New translations messages.json (Bengali)

* New translations messages.json (Hindi)

* New translations messages.json (Azerbaijani)

* New translations messages.json (Latvian)

* New translations messages.json (Estonian)

* New translations messages.json (Norwegian Nynorsk)

* New translations messages.json (Croatian)

* New translations messages.json (Indonesian)

* New translations messages.json (Portuguese, Brazilian)

* New translations messages.json (English, India)
2021-10-19 12:26:39 -04:00
github-actions[bot]
ff11d0468c Autosync the updated translations (#1241)
Co-authored-by: github-actions <>
2021-10-19 12:26:28 -04:00
Matt Gibson
825b174007 Remove unnecessary fallbacks (#1245)
Web is in lock-step to server version. We do not need fallbacks
since we're sure the server version will support collection permissions split.

(cherry picked from commit 1de569e64d)
2021-10-19 09:08:39 -04:00
Matt Gibson
39690a01ac Add user uses to new permission model (#1228)
(cherry picked from commit 3ee61fef96)
2021-10-18 14:53:25 -04:00
Matt Portune
905e39d060 Mobile WebAuthn connector updates (#1236)
* support for returning to app via button press and updated mobile connector UI

* added client-driven header text and fixed lint issues
2021-10-15 10:35:23 -04:00
Oscar Hinton
9f83899480 Remove max from minutes in vault timeout input (#1239)
(cherry picked from commit db9ab9f51e)
2021-10-14 15:55:13 +02:00
Oscar Hinton
a99d38a2f3 Bump jsib (#1231) (#1238) 2021-10-14 14:10:36 +02:00
Oscar Hinton
5dccf54611 Prevent disabling single org when max vault timeout policy is enabled (#1230)
(cherry picked from commit c3a910e785)
2021-10-14 09:11:11 +02:00
Oscar Hinton
ad872f4d15 Fix sso copy buttons not behaving correctly (#1234)
(cherry picked from commit 4b4b5910e3)
2021-10-14 08:49:56 +02:00
Oscar Hinton
247429ef37 Bump jsib (#1231)
(cherry picked from commit 471490f14f)
2021-10-12 14:52:27 +02:00
514 changed files with 38688 additions and 53722 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,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,32 +0,0 @@
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
## Objective
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
## Code 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-->
- **file.ext:** Description of what was changed and why
## Screenshots
<!--Required for any UI changes. Delete if not applicable-->
## 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)
- [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team)

View File

@@ -9,8 +9,8 @@ on:
required: false required: false
push: push:
branches-ignore: branches-ignore:
- "l10n_master" - 'l10n_master'
- "gh-pages" - 'gh-pages'
jobs: jobs:
cloc: cloc:
@@ -28,6 +28,7 @@ jobs:
- name: Print lines of code - name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -39,7 +40,9 @@ 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
@@ -51,13 +54,17 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: "16" node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: "~/.npm" path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
@@ -88,6 +95,7 @@ jobs:
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
if-no-files-found: error if-no-files-found: error
build-cloud: build-cloud:
name: Build Cloud zip name: Build Cloud zip
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -98,13 +106,17 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: "16" node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: "~/.npm" path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
@@ -135,6 +147,7 @@ jobs:
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
if-no-files-found: error if-no-files-found: error
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
@@ -145,13 +158,17 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: "16" node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: "~/.npm" path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
@@ -165,7 +182,7 @@ 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' 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:
@@ -211,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' if: github.ref == 'refs/heads/release'
run: docker tag bitwarden/web bitwarden/web:hotfix 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' 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
@@ -233,17 +250,18 @@ 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' if: github.ref == 'refs/heads/release'
run: docker push bitwarden/web:hotfix 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' if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/release'
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
@@ -251,13 +269,17 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: "16" node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: "~/.npm" path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
@@ -307,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
@@ -339,6 +361,7 @@ jobs:
- name: Log out of Docker - name: Log out of Docker
run: docker logout run: docker logout
windows: windows:
name: Test code on Windows name: Test code on Windows
runs-on: windows-2019 runs-on: windows-2019
@@ -346,7 +369,7 @@ jobs:
- 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 - name: Set up MSBuild
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
@@ -355,13 +378,17 @@ jobs:
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: "~/.npm" path: '~/.npm'
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: "16" node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
- name: Print environment - name: Print environment
run: | run: |
@@ -382,112 +409,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run linter - name: NPM install
run: npm run lint run: npm ci
- name: NPM build - name: NPM build
run: npm run build:bit:cloud run: npm run build:bit:cloud
crowdin-push:
name: Crowdin Push
if: github.ref == 'refs/heads/master'
needs:
- build-oss-selfhost
- build-cloud
- build-commercial-selfhost
- build-qa
runs-on: ubuntu-20.04
env:
_CROWDIN_PROJECT_ID: "308189"
steps:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
with:
keyvault: "bitwarden-prod-kv"
secrets: "crowdin-api-token"
- name: Upload Sources
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
with:
config: crowdin.yml
crowdin_branch_name: master
upload_sources: true
upload_translations: false
check-failures:
name: Check for failures
if: always()
runs-on: ubuntu-20.04
needs:
- cloc
- setup
- build-oss-selfhost
- build-cloud
- build-commercial-selfhost
- build-qa
- crowdin-push
- windows
steps:
- name: Check if any job failed
if: ${{ (github.ref == 'refs/heads/master') || (github.ref == 'refs/heads/rc') }}
env:
CLOC_STATUS: ${{ needs.cloc.result }}
SETUP_STATUS: ${{ needs.setup.result }}
BUILD_OSS_SELFHOST_STATUS: ${{ needs.build-oss-selfhost.result }}
BUILD_CLOUD_STATUS: ${{ needs.build-cloud.result }}
BUILD_COMMERCIAL_SELFHOST_STATUS: ${{ needs.build-commercial-selfhost.result }}
BUILD_QA_STATUS: ${{ needs.build-qa.result }}
CROWDIN_PUSH_STATUS: ${{ needs.crowdin-push.result }}
WINDOWS_STATUS: ${{ needs.windows.result }}
run: |
if [ "$CLOC_STATUS" = "failure" ]; then
exit 1
elif [ "$SETUP_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_OSS_SELFHOST_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_CLOUD_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_COMMERCIAL_SELFHOST_STATUS" = "failure" ]; then
exit 1
elif [ "$BUILD_QA_STATUS" = "failure" ]; then
exit 1
elif [ "$CROWDIN_PUSH_STATUS" = "failure" ]; then
exit 1
elif [ "$WINDOWS_STATUS" = "failure" ]; then
exit 1
fi
- name: Login to Azure - Prod Subscription
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
if: failure()
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Retrieve secrets
id: retrieve-secrets
uses: Azure/get-keyvault-secrets@80ccd3fafe5662407cc2e55f202ee34bfff8c403
if: failure()
with:
keyvault: "bitwarden-prod-kv"
secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}
with:
status: ${{ job.status }}

View File

@@ -1,15 +1,15 @@
--- ---
name: Crowdin Pull name: Crowdin Sync
on: on:
workflow_dispatch: workflow_dispatch:
inputs: {} inputs: {}
schedule: # schedule:
- cron: "0 0 * * 5" # - cron: '0 0 * * *'
jobs: jobs:
crowdin-pull: crowdin-sync:
name: Pull name: Autosync
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
env: env:
_CROWDIN_PROJECT_ID: "308189" _CROWDIN_PROJECT_ID: "308189"
@@ -30,7 +30,7 @@ jobs:
secrets: "crowdin-api-token" secrets: "crowdin-api-token"
- name: Download translations - name: Download translations
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

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,15 +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
jobs: jobs:
setup: setup:
@@ -20,13 +12,12 @@ 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 }}
steps: steps:
- name: Branch check - name: Branch check
run: | run: |
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix" ]]; then if [[ "$GITHUB_REF" != "refs/heads/release" ]]; then
echo "===================================" echo "==================================="
echo "[!] Can only release from the 'rc' or 'hotfix' branches" echo "[!] Can only release from the 'release' branch"
echo "===================================" echo "==================================="
exit 1 exit 1
fi fi
@@ -42,8 +33,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
@@ -51,18 +41,12 @@ jobs:
echo "::set-output name=package::$version" echo "::set-output name=package::$version"
echo "::set-output name=tag::v$version" echo "::set-output name=tag::v$version"
- name: Get branch name
id: branch
run: |
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME"
self-host: self-host:
name: Release self-host docker name: Release self-host docker
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 }}
steps: steps:
- name: Print environment - name: Print environment
@@ -82,28 +66,27 @@ jobs:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Pull latest selfhost image - name: Pull latest selfhost Release image
run: docker pull bitwarden/web:$_BRANCH_NAME run: docker pull bitwarden/web:latest
- name: Tag version and latest - name: Tag version
run: | run: |
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION docker tag bitwarden/web:latest bitwarden/web:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest
- name: List Docker images - name: List Docker images
run: docker images run: docker images
- name: Push version and latest image - 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 - name: Log out of Docker
run: docker logout run: docker logout
ghpages-deploy: ghpages-deploy:
name: Deploy Web Vault name: Deploy Web Vault
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -126,6 +109,8 @@ jobs:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
with:
ref: release
- name: Setup git config - name: Setup git config
run: | run: |
@@ -139,7 +124,7 @@ jobs:
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }} branch: release
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
@@ -166,6 +151,7 @@ jobs:
--base gh-pages \ --base gh-pages \
--head "$PR_BRANCH" --head "$PR_BRANCH"
release: release:
name: Create GitHub Release name: Create GitHub Release
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -179,9 +165,9 @@ jobs:
with: with:
workflow: build.yml workflow: build.yml
workflow_conclusion: success workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }} branch: release
artifacts: "web-*-selfhosted-COMMERCIAL.zip, artifacts: "web-*-selfhosted-COMMERCIAL.zip,
web-*-selfhosted-open-source.zip" web-*-selfhosted-open-source.zip"
- name: Rename assets - name: Rename assets
run: | run: |
@@ -196,6 +182,6 @@ jobs:
tag: "${{ needs.setup.outputs.tag_version }}" tag: "${{ needs.setup.outputs.tag_version }}"
body: "<insert release notes here>" body: "<insert release notes here>"
artifacts: "web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip, artifacts: "web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip,
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

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,4 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

View File

@@ -1,13 +0,0 @@
# Build directories
build
dist
jslib
# External libraries / auto synced locales
src/locales
src/404/*.min.css
src/scripts/u2f.js
# 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 [User-to-User Support 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)

View File

@@ -23,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
@@ -41,21 +41,21 @@ If you want to point the development web vault to the production APIs, you can r
``` ```
npm install npm install
ENV=cloud npm run build:oss:watch ENV=production npm run build:oss:watch
``` ```
You can also manually adjusting your API endpoint settings by adding `config/local.json` overriding any of the following values: You can also manually adjusting your API endpoint settings by adding `config/local.json` overriding any of the following values:
```json ```json
{ {
"dev": {
"proxyApi": "http://your-api-url", "proxyApi": "http://your-api-url",
"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": {}
}
} }
``` ```
@@ -66,23 +66,3 @@ Where the `urls` object is defined by the [Urls type in jslib](https://github.co
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,15 +1,15 @@
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,
}, },
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],
}) })
export class AppRoutingModule {} export class AppRoutingModule { }

View File

@@ -1,20 +1,22 @@
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() {
super.ngOnInit();
this.policyListService.addPolicies([ ngOnInit() {
new MaximumVaultTimeoutPolicy(), super.ngOnInit();
new DisablePersonalVaultExportPolicy(),
]); this.policyListService.addPolicies([
} new MaximumVaultTimeoutPolicy(),
new DisablePersonalVaultExportPolicy(),
]);
}
} }

View File

@@ -1,48 +1,44 @@
import { DragDropModule } from "@angular/cdk/drag-drop"; import { ToasterModule } from 'angular2-toaster';
import { NgModule } from "@angular/core"; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
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 { BitwardenToastModule } from "jslib-angular/components/toastr.component"; 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 { AppRoutingModule } from "./app-routing.module"; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from "./app.component"; import { AppComponent } from './app.component';
import { OrganizationsModule } from "./organizations/organizations.module"; import { OrganizationsModule } from './organizations/organizations.module';
import { DisablePersonalVaultExportPolicyComponent } from "./policies/disable-personal-vault-export.component"; import { DisablePersonalVaultExportPolicyComponent } from './policies/disable-personal-vault-export.component';
import { MaximumVaultTimeoutPolicyComponent } from "./policies/maximum-vault-timeout.component"; import { MaximumVaultTimeoutPolicyComponent } from './policies/maximum-vault-timeout.component';
import { OssRoutingModule } from "src/app/oss-routing.module"; import { OssRoutingModule } from 'src/app/oss-routing.module';
import { OssModule } from "src/app/oss.module"; import { OssModule } from 'src/app/oss.module';
import { ServicesModule } from "src/app/services/services.module"; import { ServicesModule } from 'src/app/services/services.module';
import { WildcardRoutingModule } from "src/app/wildcard-routing.module"; import { WildcardRoutingModule } from 'src/app/wildcard-routing.module';
@NgModule({ @NgModule({
imports: [ imports: [
OssModule, OssModule,
BrowserAnimationsModule, BrowserAnimationsModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
ServicesModule, ServicesModule,
BitwardenToastModule.forRoot({ ToasterModule.forRoot(),
maxOpened: 5, InfiniteScrollModule,
autoDismiss: true, DragDropModule,
closeButton: true, AppRoutingModule,
}), OssRoutingModule,
InfiniteScrollModule, OrganizationsModule,
DragDropModule, RouterModule,
AppRoutingModule, WildcardRoutingModule, // Needs to be last to catch all non-existing routes
OssRoutingModule, ],
OrganizationsModule, declarations: [
RouterModule, AppComponent,
WildcardRoutingModule, // Needs to be last to catch all non-existing routes MaximumVaultTimeoutPolicyComponent,
], DisablePersonalVaultExportPolicyComponent,
declarations: [ ],
AppComponent, bootstrap: [AppComponent],
MaximumVaultTimeoutPolicyComponent,
DisablePersonalVaultExportPolicyComponent,
],
bootstrap: [AppComponent],
}) })
export class AppModule {} export class AppModule { }

View File

@@ -1,17 +1,17 @@
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';
// tslint:disable-next-line // tslint:disable-next-line
require("src/scss/styles.scss"); 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();
} }
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true }); platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });

View File

@@ -1,488 +1,284 @@
<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 class="fa fa-spinner fa-spin text-muted" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="sr-only">{{'loading' | i18n}}</span>
</ng-container> </ng-container>
<form <form #form (ngSubmit)="submit()" [formGroup]="data" [appApiAction]="formPromise" *ngIf="!loading">
#form <div class="form-group">
(ngSubmit)="submit()" <div class="form-check">
[formGroup]="data" <input class="form-check-input" type="checkbox" id="enabled" [formControl]="enabled" name="Enabled">
[appApiAction]="formPromise" <label class="form-check-label" for="enabled">{{'enabled' | i18n}}</label>
*ngIf="!loading" </div>
ngNativeValidate
>
<p>
{{ "ssoPolicyHelpStart" | i18n }}
<a routerLink="../policies">{{ "ssoPolicyHelpLink" | i18n }}</a>
{{ "ssoPolicyHelpEnd" | i18n }}
<br />
{{ "ssoPolicyHelpKeyConnector" | i18n }}
</p>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "allowSso" | i18n }}</label>
</div> </div>
<small class="form-text text-muted">{{ "allowSsoDesc" | i18n }}</small>
</div>
<div class="form-group">
<label>{{ "memberDecryptionOption" | i18n }}</label>
<div class="form-check form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionPass"
[value]="false"
formControlName="keyConnectorEnabled"
/>
<label class="form-check-label" for="memberDecryptionPass">
{{ "masterPass" | i18n }}
<small>{{ "memberDecryptionPassDesc" | i18n }}</small>
</label>
</div>
<div class="form-check mt-2 form-check-block">
<input
class="form-check-input"
type="radio"
id="memberDecryptionKey"
[value]="true"
formControlName="keyConnectorEnabled"
[attr.disabled]="!organization.useKeyConnector || null"
/>
<label class="form-check-label" for="memberDecryptionKey">
{{ "keyConnector" | i18n }}
<a
target="_blank"
rel="noopener"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/article/about-key-connector/"
>
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
</a>
<small>{{ "memberDecryptionKeyConnectorDesc" | i18n }}</small>
</label>
</div>
</div>
<ng-container *ngIf="data.value.keyConnectorEnabled">
<app-callout type="warning" [useAlertRole]="true">
{{ "keyConnectorWarning" | i18n }}
</app-callout>
<div class="form-group"> <div class="form-group">
<label for="keyConnectorUrl">{{ "keyConnectorUrl" | i18n }}</label> <label for="type">{{'type' | i18n}}</label>
<div class="input-group"> <select class="form-control" id="type" formControlName="configType">
<input <option value="0" disabled>{{'selectType' | i18n}}</option>
class="form-control" <option value="1">OpenID Connect</option>
formControlName="keyConnectorUrl" <option value="2">SAML 2.0</option>
id="keyConnectorUrl"
required
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
(click)="validateKeyConnectorUrl()"
[disabled]="!enableTestKeyConnector"
>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="keyConnectorUrl.pending"
></i>
<span *ngIf="!keyConnectorUrl.pending">
{{ "keyConnectorTest" | i18n }}
</span>
</button>
</div>
</div>
<ng-container *ngIf="keyConnectorUrl.pristine && !keyConnectorUrl.pending">
<div class="text-danger" *ngIf="keyConnectorUrl.hasError('invalidUrl')" role="alert">
<i class="fa fa-exclamation-circle" aria-hidden="true"></i>
{{ "keyConnectorTestFail" | i18n }}
</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>
</ng-container>
<div class="form-group">
<label for="type">{{ "type" | i18n }}</label>
<select class="form-control" id="type" formControlName="configType">
<option [ngValue]="0" disabled>{{ "selectType" | i18n }}</option>
<option [ngValue]="1">OpenID Connect</option>
<option [ngValue]="2">SAML 2.0</option>
</select>
</div>
<!-- OIDC -->
<div *ngIf="data.value.configType == 1">
<div class="config-section">
<h2>{{ "openIdConnectConfig" | i18n }}</h2>
<div class="form-group">
<label>{{ "callbackPath" | i18n }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="callbackPath" />
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(callbackPath)"
>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{ "signedOutCallbackPath" | i18n }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="signedOutCallbackPath" />
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(signedOutCallbackPath)"
>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label for="authority">{{ "authority" | i18n }}</label>
<input class="form-control" formControlName="authority" id="authority" />
</div>
<div class="form-group">
<label for="clientId">{{ "clientId" | i18n }}</label>
<input class="form-control" formControlName="clientId" id="clientId" />
</div>
<div class="form-group">
<label for="clientSecret">{{ "clientSecret" | i18n }}</label>
<input class="form-control" formControlName="clientSecret" id="clientSecret" />
</div>
<div class="form-group">
<label for="metadataAddress">{{ "metadataAddress" | i18n }}</label>
<input class="form-control" formControlName="metadataAddress" id="metadataAddress" />
</div>
<div class="form-group">
<label for="redirectBehavior">{{ "oidcRedirectBehavior" | i18n }}</label>
<select class="form-control" formControlName="redirectBehavior" id="redirectBehavior">
<option [ngValue]="0">Redirect GET</option>
<option [ngValue]="1">Form POST</option>
</select> </select>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="getClaimsFromUserInfoEndpoint"
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 *ngIf="data.value.configType == 2">
<!-- SAML2 SP -->
<div class="config-section">
<h2>{{ "samlSpConfig" | i18n }}</h2>
<div class="form-group">
<label>{{ "spEntityId" | i18n }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="spEntityId" />
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(spEntityId)"
>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{ "spMetadataUrl" | i18n }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="spMetadataUrl" />
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launchUri(spMetadataUrl)"
>
<i class="fa fa-lg fa-external-link" aria-hidden="true"></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(spMetadataUrl)"
>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{ "spAcsUrl" | i18n }}</label>
<div class="input-group">
<input class="form-control" readonly [value]="spAcsUrl" />
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(spAcsUrl)"
>
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label for="spNameIdFormat">{{ "spNameIdFormat" | i18n }}</label>
<select class="form-control" formControlName="spNameIdFormat" id="spNameIdFormat">
<option value="0">Not Configured</option>
<option value="1">Unspecified</option>
<option value="2">Email Address</option>
<option value="3">X.509 Subject Name</option>
<option value="4">Windows Domain Qualified Name</option>
<option value="5">Kerberos Principal Name</option>
<option value="6">Entity Identifier</option>
<option value="7">Persistent</option>
<option value="8">Transient</option>
</select>
</div>
<div class="form-group">
<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 --> <!-- OIDC -->
<div class="config-section"> <div *ngIf="data.value.configType == 1">
<h2>{{ "samlIdpConfig" | i18n }}</h2> <div class="config-section">
<h2>{{'openIdConnectConfig' | i18n}}</h2>
<div class="form-group"> <div class="form-group">
<label for="idpEntityId">{{ "idpEntityId" | i18n }}</label> <label>{{'callbackPath' | i18n}}</label>
<input class="form-control" formControlName="idpEntityId" id="idpEntityId" /> <div class="input-group">
</div> <input class="form-control" readonly [value]="callbackPath">
<div class="form-group"> <div class="input-group-append">
<label for="idpBindingType">{{ "idpBindingType" | i18n }}</label> <button type="button" class="btn btn-outline-secondary"
<select class="form-control" formControlName="idpBindingType" id="idpBindingType"> appA11yTitle="{{'copyValue' | i18n}}"
<option value="1">Redirect</option> (click)="copy(callbackPath)">
<option value="2">HTTP POST</option> <i class="fa fa-lg fa-clone" aria-hidden="true"></i>
<option value="4">Artifact</option> </button>
</select> </div>
</div> </div>
<div class="form-group"> </div>
<label for="idpSingleSignOnServiceUrl">{{ "idpSingleSignOnServiceUrl" | i18n }}</label> <div class="form-group">
<input <label>{{'signedOutCallbackPath' | i18n}}</label>
class="form-control" <div class="input-group">
formControlName="idpSingleSignOnServiceUrl" <input class="form-control" readonly [value]="signedOutCallbackPath">
id="idpSingleSignOnServiceUrl" <div class="input-group-append">
/> <button type="button" class="btn btn-outline-secondary"
</div> appA11yTitle="{{'copyValue' | i18n}}"
<div class="form-group"> (click)="copy(signedOutCallbackPath)">
<label for="idpSingleLogoutServiceUrl">{{ "idpSingleLogoutServiceUrl" | i18n }}</label> <i class="fa fa-lg fa-clone" aria-hidden="true"></i>
<input </button>
class="form-control" </div>
formControlName="idpSingleLogoutServiceUrl" </div>
id="idpSingleLogoutServiceUrl" </div>
/> <div class="form-group">
</div> <label>{{'authority' | i18n}}</label>
<div class="form-group"> <input class="form-control" formControlName="authority">
<label for="idpArtifactResolutionServiceUrl">{{ </div>
"idpArtifactResolutionServiceUrl" | i18n <div class="form-group">
}}</label> <label>{{'clientId' | i18n}}</label>
<input <input class="form-control" formControlName="clientId">
class="form-control" </div>
formControlName="idpArtifactResolutionServiceUrl" <div class="form-group">
id="idpArtifactResolutionServiceUrl" <label>{{'clientSecret' | i18n}}</label>
/> <input class="form-control" formControlName="clientSecret">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="idpX509PublicCert">{{ "idpX509PublicCert" | i18n }}</label> <label>{{'metadataAddress' | i18n}}</label>
<textarea <input class="form-control" formControlName="metadataAddress">
formControlName="idpX509PublicCert" </div>
class="form-control form-control-sm text-monospace" <div class="form-group">
rows="6" <label>{{'oidcRedirectBehavior' | i18n}}</label>
id="idpX509PublicCert" <select class="form-control" formControlName="redirectBehavior">
></textarea> <option value="0">Redirect GET</option>
</div> <option value="1">Form POST</option>
<div class="form-group"> </select>
<label for="idpOutboundSigningAlgorithm">{{ "idpOutboundSigningAlgorithm" | i18n }}</label> </div>
<select <div class="form-group">
class="form-control" <div class="form-check">
formControlName="idpOutboundSigningAlgorithm" <input class="form-check-input" type="checkbox" id="getClaimsFromUserInfoEndpoint"
id="idpOutboundSigningAlgorithm" formControlName="getClaimsFromUserInfoEndpoint">
> <label class="form-check-label" for="getClaimsFromUserInfoEndpoint">
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option> {{'getClaimsFromUserInfoEndpoint' | i18n}}
</select> </label>
</div> </div>
<div class="form-group"> </div>
<div class="form-check"> <div class="form-group">
<input <label>{{'additionalScopes' | i18n}}</label>
class="form-check-input" <input class="form-control" formControlName="additionalScopes">
type="checkbox" </div>
id="idpAllowUnsolicitedAuthnResponse" <div class="form-group">
formControlName="idpAllowUnsolicitedAuthnResponse" <label>{{'additionalUserIdClaimTypes' | i18n}}</label>
/> <input class="form-control" formControlName="additionalUserIdClaimTypes">
<label class="form-check-label" for="idpAllowUnsolicitedAuthnResponse"> </div>
{{ "idpAllowUnsolicitedAuthnResponse" | i18n }} <div class="form-group">
</label> <label>{{'additionalEmailClaimTypes' | i18n}}</label>
<input class="form-control" formControlName="additionalEmailClaimTypes">
</div>
<div class="form-group">
<label>{{'additionalNameClaimTypes' | i18n}}</label>
<input class="form-control" formControlName="additionalNameClaimTypes">
</div>
<div class="form-group">
<label>{{'acrValues' | i18n}}</label>
<input class="form-control" formControlName="acrValues">
</div>
<div class="form-group">
<label>{{'expectedReturnAcrValue' | i18n}}</label>
<input class="form-control" formControlName="expectedReturnAcrValue">
</div>
</div> </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> </div>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> <div *ngIf="data.value.configType == 2">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <!-- SAML2 SP -->
<span>{{ "save" | i18n }}</span> <div class="config-section">
</button> <h2>{{'samlSpConfig' | i18n}}</h2>
<div class="form-group">
<label>{{'spEntityId' | i18n}}</label>
<div class="input-group">
<input class="form-control" readonly [value]="spEntityId" >
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyValue' | i18n}}"
(click)="copy(spEntityId)">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{'spMetadataUrl' | i18n}}</label>
<div class="input-group">
<input class="form-control" readonly [value]="spMetadataUrl">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'launch' | i18n}}"
(click)="launchUri(spMetadataUrl)">
<i class="fa fa-lg fa-external-link" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyValue' | i18n}}"
(click)="copy(spMetadataUrl)">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{'spAcsUrl' | i18n}}</label>
<div class="input-group">
<input class="form-control" readonly [value]="spAcsUrl">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary"
appA11yTitle="{{'copyValue' | i18n}}"
(click)="copy(spAcsUrl)">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label>{{'spNameIdFormat' | i18n}}</label>
<select class="form-control" formControlName="spNameIdFormat">
<option value="0">Not Configured</option>
<option value="1">Unspecified</option>
<option value="2">Email Address</option>
<option value="3">X.509 Subject Name</option>
<option value="4">Windows Domain Qualified Name</option>
<option value="5">Kerberos Principal Name</option>
<option value="6">Entity Identifier</option>
<option value="7">Persistent</option>
<option value="8">Transient</option>
</select>
</div>
<div class="form-group">
<label>{{'spOutboundSigningAlgorithm' | i18n}}</label>
<select class="form-control" formControlName="spOutboundSigningAlgorithm">
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
</select>
</div>
<div class="form-group">
<label>{{'spSigningBehavior' | i18n}}</label>
<select class="form-control" formControlName="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>{{'spMinIncomingSigningAlgorithm' | i18n}}</label>
<select class="form-control" formControlName="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>
<!-- SAML2 IDP -->
<div class="config-section">
<h2>{{'samlIdpConfig' | i18n}}</h2>
<div class="form-group">
<label>{{'idpEntityId' | i18n}}</label>
<input class="form-control" formControlName="idpEntityId">
</div>
<div class="form-group">
<label>{{'idpBindingType' | i18n}}</label>
<select class="form-control" formControlName="idpBindingType">
<option value="1">Redirect</option>
<option value="2">HTTP POST</option>
<option value="4">Artifact</option>
</select>
</div>
<div class="form-group">
<label>{{'idpSingleSignOnServiceUrl' | i18n}}</label>
<input class="form-control" formControlName="idpSingleSignOnServiceUrl">
</div>
<div class="form-group">
<label>{{'idpSingleLogoutServiceUrl' | i18n}}</label>
<input class="form-control" formControlName="idpSingleLogoutServiceUrl">
</div>
<div class="form-group">
<label>{{'idpArtifactResolutionServiceUrl' | i18n}}</label>
<input class="form-control" formControlName="idpArtifactResolutionServiceUrl">
</div>
<div class="form-group">
<label>{{'idpX509PublicCert' | i18n}}</label>
<textarea formControlName="idpX509PublicCert" class="form-control form-control-sm text-monospace" rows="6"></textarea>
</div>
<div class="form-group">
<label>{{'idpOutboundSigningAlgorithm' | i18n}}</label>
<select class="form-control" formControlName="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>
</div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'save' | i18n}}</span>
</button>
</form> </form>

View File

@@ -1,183 +1,121 @@
import { Component, OnInit } from "@angular/core"; import {
import { FormBuilder } from "@angular/forms"; Component,
import { ActivatedRoute } from "@angular/router"; OnInit,
} from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
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 { OrganizationService } from "jslib-common/abstractions/organization.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { OrganizationSsoRequest } from 'jslib-common/models/request/organization/organizationSsoRequest';
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 {
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",
];
loading = true; samlSigningAlgorithms = [
organizationId: string; 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
organization: Organization; 'http://www.w3.org/2000/09/xmldsig#rsa-sha384',
formPromise: Promise<any>; 'http://www.w3.org/2000/09/xmldsig#rsa-sha512',
'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
];
callbackPath: string; loading = true;
signedOutCallbackPath: string; organizationId: string;
spEntityId: string; formPromise: Promise<any>;
spMetadataUrl: string;
spAcsUrl: string;
enabled = this.formBuilder.control(false); callbackPath: string;
data = this.formBuilder.group({ signedOutCallbackPath: string;
configType: [], spEntityId: string;
spMetadataUrl: string;
spAcsUrl: string;
keyConnectorEnabled: [], enabled = this.fb.control(false);
keyConnectorUrl: [], data = this.fb.group({
configType: [],
// OpenId // OpenId
authority: [], authority: [],
clientId: [], clientId: [],
clientSecret: [], clientSecret: [],
metadataAddress: [], metadataAddress: [],
redirectBehavior: [], redirectBehavior: [],
getClaimsFromUserInfoEndpoint: [], getClaimsFromUserInfoEndpoint: [],
additionalScopes: [], additionalScopes: [],
additionalUserIdClaimTypes: [], additionalUserIdClaimTypes: [],
additionalEmailClaimTypes: [], additionalEmailClaimTypes: [],
additionalNameClaimTypes: [], additionalNameClaimTypes: [],
acrValues: [], acrValues: [],
expectedReturnAcrValue: [], expectedReturnAcrValue: [],
// SAML // SAML
spNameIdFormat: [], spNameIdFormat: [],
spOutboundSigningAlgorithm: [], spOutboundSigningAlgorithm: [],
spSigningBehavior: [], spSigningBehavior: [],
spMinIncomingSigningAlgorithm: [], spMinIncomingSigningAlgorithm: [],
spWantAssertionsSigned: [], spWantAssertionsSigned: [],
spValidateCertificates: [], spValidateCertificates: [],
idpEntityId: [], idpEntityId: [],
idpBindingType: [], idpBindingType: [],
idpSingleSignOnServiceUrl: [], idpSingleSignOnServiceUrl: [],
idpSingleLogoutServiceUrl: [], idpSingleLogoutServiceUrl: [],
idpArtifactResolutionServiceUrl: [], idpArtifactResolutionServiceUrl: [],
idpX509PublicCert: [], idpX509PublicCert: [],
idpOutboundSigningAlgorithm: [], idpOutboundSigningAlgorithm: [],
idpAllowUnsolicitedAuthnResponse: [], idpAllowUnsolicitedAuthnResponse: [],
idpDisableOutboundLogoutRequests: [], idpDisableOutboundLogoutRequests: [],
idpWantAuthnRequestsSigned: [], idpWantAuthnRequestsSigned: [],
});
constructor(
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private apiService: ApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private organizationService: OrganizationService
) {}
async ngOnInit() {
this.route.parent.parent.params.subscribe(async (params) => {
this.organizationId = params.organizationId;
await this.load();
}); });
}
async load() { constructor(private fb: FormBuilder, private route: ActivatedRoute, private apiService: ApiService,
this.organization = await this.organizationService.get(this.organizationId); private platformUtilsService: PlatformUtilsService, private i18nService: I18nService) { }
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
this.data.patchValue(ssoSettings.data); async ngOnInit() {
this.enabled.setValue(ssoSettings.enabled); this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
this.callbackPath = ssoSettings.urls.callbackPath; await this.load();
this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath; });
this.spEntityId = ssoSettings.urls.spEntityId;
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
this.keyConnectorUrl.markAsDirty();
this.loading = false;
}
copy(value: string) {
this.platformUtilsService.copyToClipboard(value);
}
launchUri(url: string) {
this.platformUtilsService.launchUri(url);
}
async submit() {
this.formPromise = this.postData();
try {
const response = await this.formPromise;
this.data.patchValue(response.data);
this.enabled.setValue(response.enabled);
this.platformUtilsService.showToast("success", null, this.i18nService.t("ssoSettingsSaved"));
} catch {
// Logged by appApiAction, do nothing
} }
this.formPromise = null; async load() {
} const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
async postData() { this.data.patchValue(ssoSettings.data);
if (this.data.get("keyConnectorEnabled").value) { this.enabled.setValue(ssoSettings.enabled);
await this.validateKeyConnectorUrl();
if (this.keyConnectorUrl.hasError("invalidUrl")) { this.callbackPath = ssoSettings.urls.callbackPath;
throw new Error(this.i18nService.t("keyConnectorTestFail")); this.signedOutCallbackPath = ssoSettings.urls.signedOutCallbackPath;
} this.spEntityId = ssoSettings.urls.spEntityId;
this.spMetadataUrl = ssoSettings.urls.spMetadataUrl;
this.spAcsUrl = ssoSettings.urls.spAcsUrl;
this.loading = false;
} }
const request = new OrganizationSsoRequest(); copy(value: string) {
request.enabled = this.enabled.value; this.platformUtilsService.copyToClipboard(value);
request.data = this.data.value;
return this.apiService.postOrganizationSso(this.organizationId, request);
}
async validateKeyConnectorUrl() {
if (this.keyConnectorUrl.pristine) {
return;
} }
this.keyConnectorUrl.markAsPending(); launchUri(url: string) {
this.platformUtilsService.launchUri(url);
try {
await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
this.keyConnectorUrl.updateValueAndValidity();
} catch {
this.keyConnectorUrl.setErrors({
invalidUrl: true,
});
} }
this.keyConnectorUrl.markAsPristine(); async submit() {
} const request = new OrganizationSsoRequest();
request.enabled = this.enabled.value;
request.data = this.data.value;
get enableTestKeyConnector() { this.formPromise = this.apiService.postOrganizationSso(this.organizationId, request);
return (
this.data.get("keyConnectorEnabled").value &&
this.keyConnectorUrl != null &&
this.keyConnectorUrl.value !== ""
);
}
get keyConnectorUrl() { const response = await this.formPromise;
return this.data.get("keyConnectorUrl"); this.data.patchValue(response.data);
} this.enabled.setValue(response.enabled);
this.formPromise = null;
this.platformUtilsService.showToast('success', null, this.i18nService.t('ssoSettingsSaved'));
}
} }

View File

@@ -1,54 +1,54 @@
import { NgModule } from "@angular/core"; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from "@angular/router"; import { RouterModule, Routes } from '@angular/router';
import { AuthGuardService } from "jslib-angular/services/auth-guard.service"; import { AuthGuardService } from 'jslib-angular/services/auth-guard.service';
import { Permissions } from "jslib-common/enums/permissions"; import { Permissions } from 'jslib-common/enums/permissions';
import { OrganizationLayoutComponent } from "src/app/layouts/organization-layout.component"; import { OrganizationLayoutComponent } from 'src/app/layouts/organization-layout.component';
import { ManageComponent } from "src/app/organizations/manage/manage.component"; import { ManageComponent } from 'src/app/organizations/manage/manage.component';
import { OrganizationGuardService } from "src/app/services/organization-guard.service"; import { OrganizationGuardService } from 'src/app/services/organization-guard.service';
import { OrganizationTypeGuardService } from "src/app/services/organization-type-guard.service"; import { OrganizationTypeGuardService } from 'src/app/services/organization-type-guard.service';
import { SsoComponent } from "./manage/sso.component"; import { SsoComponent } from './manage/sso.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: "organizations/:organizationId", path: 'organizations/:organizationId',
component: OrganizationLayoutComponent, component: OrganizationLayoutComponent,
canActivate: [AuthGuardService, OrganizationGuardService], canActivate: [AuthGuardService, OrganizationGuardService],
children: [
{
path: "manage",
component: ManageComponent,
canActivate: [OrganizationTypeGuardService],
data: {
permissions: [
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: 'manage',
component: SsoComponent, component: ManageComponent,
}, canActivate: [OrganizationTypeGuardService],
data: {
permissions: [
Permissions.CreateNewCollections,
Permissions.EditAnyCollection,
Permissions.DeleteAnyCollection,
Permissions.EditAssignedCollections,
Permissions.DeleteAssignedCollections,
Permissions.AccessEventLogs,
Permissions.ManageGroups,
Permissions.ManageUsers,
Permissions.ManagePolicies,
Permissions.ManageSso,
],
},
children: [
{
path: 'sso',
component: SsoComponent,
},
],
},
], ],
}, },
],
},
]; ];
@NgModule({ @NgModule({
imports: [RouterModule.forChild(routes)], imports: [RouterModule.forChild(routes)],
exports: [RouterModule], exports: [RouterModule],
}) })
export class OrganizationsRoutingModule {} export class OrganizationsRoutingModule { }

View File

@@ -1,14 +1,22 @@
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 { OssModule } from "src/app/oss.module"; import { OssModule } from 'src/app/oss.module';
import { SsoComponent } from "./manage/sso.component"; import { SsoComponent } from './manage/sso.component';
import { OrganizationsRoutingModule } from "./organizations-routing.module"; import { OrganizationsRoutingModule } from './organizations-routing.module';
@NgModule({ @NgModule({
imports: [CommonModule, FormsModule, ReactiveFormsModule, OssModule, OrganizationsRoutingModule], imports: [
declarations: [SsoComponent], CommonModule,
FormsModule,
ReactiveFormsModule,
OssModule,
OrganizationsRoutingModule,
],
declarations: [
SsoComponent,
],
}) })
export class OrganizationsModule {} export class OrganizationsModule {}

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" </div>
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
</div>
</div> </div>

View File

@@ -1,26 +1,24 @@
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 { PolicyType } from 'jslib-common/enums/policyType';
import { PolicyRequest } from "jslib-common/models/request/policyRequest"; import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
import { import { BasePolicy, BasePolicyComponent } from 'src/app/organizations/policies/base-policy.component';
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,47 +1,27 @@
<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" </div>
id="enabled"
[formControl]="enabled"
name="Enabled"
/>
<label class="form-check-label" for="enabled">{{ "enabled" | i18n }}</label>
</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" </div>
type="number" <div class="col-6">
min="0" <input id="minutes" class="form-control" type="number" min="0" max="59" name="minutes"
name="hours" formControlName="minutes">
formControlName="hours" <small>{{'minutes' | i18n }}</small>
/> </div>
<small>{{ "hours" | i18n }}</small> </div>
</div>
<div class="col-6">
<input
id="minutes"
class="form-control"
type="number"
min="0"
max="59"
name="minutes"
formControlName="minutes"
/>
<small>{{ "minutes" | i18n }}</small>
</div>
</div> </div>
</div>
</div> </div>

View File

@@ -1,72 +1,70 @@
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 { PolicyType } from 'jslib-common/enums/policyType';
import { PolicyRequest } from "jslib-common/models/request/policyRequest"; import { PolicyRequest } from 'jslib-common/models/request/policyRequest';
import { import { BasePolicy, BasePolicyComponent } from 'src/app/organizations/policies/base-policy.component';
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({
hours: [null],
minutes: [null],
});
constructor(private formBuilder: FormBuilder, private i18nService: I18nService) { data = this.fb.group({
super(); hours: [null],
} minutes: [null],
loadData() {
const minutes = this.policyResponse.data?.minutes;
if (minutes == null) {
return;
}
this.data.patchValue({
hours: Math.floor(minutes / 60),
minutes: minutes % 60,
}); });
}
buildRequestData() { constructor(private fb: FormBuilder, private i18nService: I18nService) {
if (this.data.value.hours == null && this.data.value.minutes == null) { super();
return null;
} }
return { loadData() {
minutes: this.data.value.hours * 60 + this.data.value.minutes, const minutes = this.policyResponse.data?.minutes;
};
}
buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> { if (minutes == null) {
const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false; return;
if (this.enabled.value && !singleOrgEnabled) { }
throw new Error(this.i18nService.t("requireSsoPolicyReqError"));
this.data.patchValue({
hours: Math.floor(minutes / 60),
minutes: minutes % 60,
});
} }
const data = this.buildRequestData(); buildRequestData() {
if (data?.minutes == null || data?.minutes <= 0) { if (this.data.value.hours == null && this.data.value.minutes == null) {
throw new Error(this.i18nService.t("invalidMaximumVaultTimeout")); return null;
}
return {
minutes: this.data.value.hours * 60 + this.data.value.minutes,
};
} }
return super.buildRequest(policiesEnabledMap); buildRequest(policiesEnabledMap: Map<PolicyType, boolean>): Promise<PolicyRequest> {
} const singleOrgEnabled = policiesEnabledMap.get(PolicyType.SingleOrg) ?? false;
if (this.enabled.value && !singleOrgEnabled) {
throw new Error(this.i18nService.t('requireSsoPolicyReqError'));
}
const data = this.buildRequestData();
if (data?.minutes == null || data?.minutes <= 0) {
throw new Error(this.i18nService.t('invalidMaximumVaultTimeout'));
}
return super.buildRequest(policiesEnabledMap);
}
} }

View File

@@ -1,46 +1,35 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="addTitle"> <div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="addTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document"> <div class="modal-dialog modal-dialog-scrollable" role="document">
<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" <span aria-hidden="true">&times;</span>
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="card-body text-center" *ngIf="loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<ng-container *ngIf="!loading">
<table class="table table-hover table-list">
<tr *ngFor="let o of organizations">
<td width="30">
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
{{ o.name }}
</td>
<td>
<button
class="btn btn-outline-secondary pull-right"
(click)="add(o)"
[disabled]="formPromise"
>
Add
</button> </button>
</td> </div>
</tr> <div class="modal-body">
</table> <div class="card-body text-center" *ngIf="loading">
</ng-container> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</div> {{'loading' | i18n}}
</div>
<ng-container *ngIf="!loading">
<table class="table table-hover table-list">
<tr *ngFor="let o of organizations">
<td width="30">
<app-avatar [data]="o.name" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
{{o.name}}
</td>
<td>
<button class="btn btn-outline-secondary pull-right" (click)="add(o)" [disabled]="formPromise">Add</button>
</td>
</tr>
</table>
</ng-container>
</div>
</div>
</div> </div>
</div>
</div> </div>

View File

@@ -1,86 +1,83 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import {
Component,
EventEmitter,
Input,
OnInit,
Output
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { I18nService } from "jslib-common/abstractions/i18n.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 { ProviderService } from "jslib-common/abstractions/provider.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ValidationService } from "jslib-angular/services/validation.service"; import { ValidationService } from 'jslib-angular/services/validation.service';
import { WebProviderService } from "../services/webProvider.service"; import { ProviderService } from '../services/provider.service';
import { Organization } from "jslib-common/models/domain/organization"; import { Organization } from 'jslib-common/models/domain/organization';
import { Provider } from "jslib-common/models/domain/provider"; 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() organizations: Organization[];
@Output() onAddedOrganization = new EventEmitter();
provider: Provider; @Input() providerId: string;
formPromise: Promise<any>; @Input() organizations: Organization[];
loading = true; @Output() onAddedOrganization = new EventEmitter();
constructor( provider: Provider;
private providerService: ProviderService, formPromise: Promise<any>;
private webProviderService: WebProviderService, loading = true;
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private validationService: ValidationService
) {}
async ngOnInit() { constructor(private userService: UserService, private providerService: ProviderService,
await this.load(); private toasterService: ToasterService, private i18nService: I18nService,
} private platformUtilsService: PlatformUtilsService, private validationService: ValidationService,
private apiService: ApiService) { }
async load() { async ngOnInit() {
if (this.providerId == null) { await this.load();
return;
} }
this.provider = await this.providerService.get(this.providerId); async load() {
if (this.providerId == null) {
return;
}
this.loading = false; this.provider = await this.userService.getProvider(this.providerId);
}
async add(organization: Organization) { this.loading = false;
if (this.formPromise) {
return;
} }
const confirmed = await this.platformUtilsService.showDialog( async add(organization: Organization) {
this.i18nService.t("addOrganizationConfirmation", organization.name, this.provider.name), if (this.formPromise) {
organization.name, return;
this.i18nService.t("yes"), }
this.i18nService.t("no"),
"warning"
);
if (!confirmed) { const confirmed = await this.platformUtilsService.showDialog(
return false; this.i18nService.t('addOrganizationConfirmation', organization.name, this.provider.name), organization.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
try {
this.formPromise = this.providerService.addOrganizationToProvider(this.providerId, organization.id);
await this.formPromise;
} catch (e) {
this.validationService.showError(e);
return;
} finally {
this.formPromise = null;
}
this.toasterService.popAsync('success', null, this.i18nService.t('organizationJoinedProvider'));
this.onAddedOrganization.emit();
} }
try {
this.formPromise = this.webProviderService.addOrganizationToProvider(
this.providerId,
organization.id
);
await this.formPromise;
} catch (e) {
this.validationService.showError(e);
return;
} finally {
this.formPromise = null;
}
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("organizationJoinedProvider")
);
this.onAddedOrganization.emit();
}
} }

View File

@@ -1,86 +1,62 @@
<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" </div>
id="search" <a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
placeholder="{{ 'search' | i18n }}" <i class="fa fa-plus fa-fw" aria-hidden="true"></i>
[(ngModel)]="searchText" {{'newClientOrganization' | i18n}}
/> </a>
<button class="btn btn-sm btn-outline-primary ml-3" (click)="addExistingOrganization()"
*ngIf="manageOrganizations && showAddExisting">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{'addExistingOrganization' | i18n}}
</button>
</div> </div>
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{ "newClientOrganization" | i18n }}
</a>
<button
class="btn btn-sm btn-outline-primary ml-3"
(click)="addExistingOrganization()"
*ngIf="manageOrganizations && showAddExisting"
>
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
{{ "addExistingOrganization" | i18n }}
</button>
</div>
</div> </div>
<ng-container *ngIf="loading"> <ng-container *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <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 class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
<table [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
class="table table-hover table-list" <tbody>
infiniteScroll <tr *ngFor="let o of searchedClients">
[infiniteScrollDistance]="1" <td width="30">
[infiniteScrollDisabled]="!isPaging()" <app-avatar [data]="o.organizationName" size="25" [circle]="true" [fontSize]="14"></app-avatar>
(scrolled)="loadMore()" </td>
> <td>
<tbody> <a [routerLink]="['/organizations', o.organizationId]">{{o.organizationName}}</a>
<tr *ngFor="let o of searchedClients"> </td>
<td width="30"> <td class="table-list-options" *ngIf="manageOrganizations">
<app-avatar <div class="dropdown" appListDropdown>
[data]="o.organizationName" <button class="btn btn-outline-secondary dropdown-toggle" type="button"
size="25" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
[circle]="true" appA11yTitle="{{'options' | i18n}}">
[fontSize]="14" <i class="fa fa-cog fa-lg" aria-hidden="true"></i>
></app-avatar> </button>
</td> <div class="dropdown-menu dropdown-menu-right">
<td> <a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
<a [routerLink]="['/organizations', o.organizationId]">{{ o.organizationName }}</a> <i class="fa fa-fw fa-remove" aria-hidden="true"></i>
</td> {{'remove' | i18n}}
<td class="table-list-options" *ngIf="manageOrganizations"> </a>
<div class="dropdown" appListDropdown> </div>
<button </div>
class="btn btn-outline-secondary dropdown-toggle" </td>
type="button" </tr>
data-toggle="dropdown" </tbody>
aria-haspopup="true" </table>
aria-expanded="false" </ng-container>
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
</button>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item text-danger" href="#" appStopClick (click)="remove(o)">
<i class="fa fa-fw fa-remove" aria-hidden="true"></i>
{{ "remove" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container> </ng-container>
<ng-template #add></ng-template> <ng-template #add></ng-template>

View File

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

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,23 +1,26 @@
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,42 +1,35 @@
<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="fa fa-spinner fa-spin fa-2x text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}" </p>
aria-hidden="true" </div>
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</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 routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block"> <a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{ "logIn" | i18n }} {{'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" </a>
> </div>
{{ "createAccount" | i18n }} </div>
</a> </div>
</div>
</div> </div>
</div>
</div> </div>
</div>
</div> </div>

View File

@@ -1,56 +1,48 @@
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 { BaseAcceptComponent } from "src/app/common/base.accept.component"; import { BaseAcceptComponent } from 'src/app/common/base.accept.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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { StateService } from 'jslib-common/abstractions/state.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"; 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) {
this.providerName = qParams.providerName; this.providerName = qParams.providerName;
} }
} }

View File

@@ -1,34 +1,38 @@
import { Component, Input } from "@angular/core"; import {
Component,
Input,
} from '@angular/core';
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 { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType"; import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "src/app/organizations/manage/bulk/bulk-confirm.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"; 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;
protected isAccepted(user: BulkUserDetails) { @Input() providerId: string;
return user.status === ProviderUserStatusType.Accepted;
}
protected async getPublicKeys() { protected isAccepted(user: BulkUserDetails) {
const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id)); return user.status === ProviderUserStatusType.Accepted;
return await this.apiService.postProviderUsersPublicKey(this.providerId, request); }
}
protected getCryptoKey() { protected async getPublicKeys() {
return this.cryptoService.getProviderKey(this.providerId); const request = new ProviderUserBulkRequest(this.filteredUsers.map(user => user.id));
} return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
}
protected async postConfirmRequest(userIdsWithKeys: any[]) { protected getCryptoKey() {
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys); return this.cryptoService.getProviderKey(this.providerId);
return await this.apiService.postProviderUserBulkConfirm(this.providerId, request); }
}
protected async postConfirmRequest(userIdsWithKeys: any[]) {
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys);
return await this.apiService.postProviderUserBulkConfirm(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;
async deleteUsers() { @Input() providerId: string;
const request = new ProviderUserBulkRequest(this.users.map((user) => user.id));
return await this.apiService.deleteManyProviderUsers(this.providerId, request); async deleteUsers() {
} const request = new ProviderUserBulkRequest(this.users.map(user => user.id));
return await this.apiService.deleteManyProviderUsers(this.providerId, request);
}
} }

View File

@@ -1,103 +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" <span class="mx-2">-</span>
placeholder="{{ 'startDate' | i18n }}" <label class="sr-only" for="end">{{'endDate' | i18n}}</label>
[(ngModel)]="start" <input type="datetime-local" class="form-control form-control-sm" id="end"
placeholder="YYYY-MM-DDTHH:MM" placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true" (change)="dirtyDates = true">
/> </div>
<span class="mx-2">-</span> <form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
<label class="sr-only" for="end">{{ "endDate" | i18n }}</label> <button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="loadEvents(true)"
<input [disabled]="loaded && refreshForm.loading">
type="datetime-local" <i class="fa fa-refresh fa-fw" aria-hidden="true" [ngClass]="{'fa-spin': loaded && refreshForm.loading}"></i>
class="form-control form-control-sm" {{'refresh' | i18n}}
id="end" </button>
placeholder="{{ 'endDate' | i18n }}" </form>
[(ngModel)]="end" <form #exportForm [appApiAction]="exportPromise" class="d-inline">
placeholder="YYYY-MM-DDTHH:MM" <button type="button" class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
(change)="dirtyDates = true" [ngClass]="{loading:exportForm.loading}" (click)="exportEvents()"
/> [disabled]="loaded && exportForm.loading || dirtyDates">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<span>{{'export' | i18n}}</span>
</button>
</form>
</div> </div>
<form #refreshForm [appApiAction]="refreshPromise" class="d-inline">
<button
type="button"
class="btn btn-sm btn-outline-primary ml-3"
(click)="loadEvents(true)"
[disabled]="loaded && refreshForm.loading"
>
<i
class="fa fa-refresh fa-fw"
aria-hidden="true"
[ngClass]="{ 'fa-spin': loaded && refreshForm.loading }"
></i>
{{ "refresh" | i18n }}
</button>
</form>
<form #exportForm [appApiAction]="exportPromise" class="d-inline">
<button
type="button"
class="btn btn-sm btn-outline-primary btn-submit manual ml-3"
[ngClass]="{ loading: exportForm.loading }"
(click)="exportEvents()"
[disabled]="(loaded && exportForm.loading) || dirtyDates"
>
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
<span>{{ "export" | i18n }}</span>
</button>
</form>
</div>
</div> </div>
<ng-container *ngIf="!loaded"> <ng-container *ngIf="!loaded">
<i class="fa fa-spinner fa-spin text-muted" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <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 fa fa-lg {{ e.appIcon }}" <span class="sr-only">{{e.appName}}, {{e.ip}}</span>
title="{{ e.appName }}, {{ e.ip }}" </td>
aria-hidden="true" <td>
></i> <span title="{{e.userEmail}}">{{e.userName}}</span>
<span class="sr-only">{{ e.appName }}, {{ e.ip }}</span> </td>
</td> <td [innerHTML]="e.message"></td>
<td> </tr>
<span title="{{ e.userEmail }}">{{ e.userName }}</span> </tbody>
</td> </table>
<td [innerHTML]="e.message"></td> <button #moreBtn [appApiAction]="morePromise" type="button" class="btn btn-block btn-link btn-submit"
</tr> (click)="loadEvents(false)" [disabled]="loaded && moreBtn.loading" *ngIf="continuationToken">
</tbody> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</table> <span>{{'loadMore' | i18n}}</span>
<button </button>
#moreBtn
[appApiAction]="morePromise"
type="button"
class="btn btn-block btn-link btn-submit"
(click)="loadEvents(false)"
[disabled]="loaded && moreBtn.loading"
*ngIf="continuationToken"
>
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "loadMore" | i18n }}</span>
</button>
</ng-container> </ng-container>

View File

@@ -1,82 +1,71 @@
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 { 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 { ProviderService } from "jslib-common/abstractions/provider.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe"; import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
import { EventResponse } from "jslib-common/models/response/eventResponse"; import { EventResponse } from 'jslib-common/models/response/eventResponse';
import { EventService } from "src/app/services/event.service"; import { EventService } from 'src/app/services/event.service';
import { BaseEventsComponent } from "src/app/common/base.events.component"; 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: string = "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();
}); });
} }
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 });
}); });
await this.loadEvents(true); await this.loadEvents(true);
this.loaded = true; this.loaded = true;
} }
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

@@ -1,30 +1,22 @@
<div class="container page-content"> <div class="container page-content">
<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" </a>
*ngIf="provider.canManageUsers" <a routerLink="events" class="list-group-item" routerLinkActive="active"
> *ngIf="provider.canAccessEventLogs && accessEvents">
{{ "people" | i18n }} {{'eventLogs' | i18n}}
</a> </a>
<a </div>
routerLink="events" </div>
class="list-group-item" </div>
routerLinkActive="active" <div class="col-9">
*ngIf="provider.canAccessEventLogs && accessEvents" <router-outlet></router-outlet>
>
{{ "eventLogs" | i18n }}
</a>
</div> </div>
</div>
</div> </div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div> </div>

View File

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

View File

@@ -1,292 +1,238 @@
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import {
import { ActivatedRoute, Router } from "@angular/router"; Component,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { first } from "rxjs/operators"; 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 { SearchService } from 'jslib-common/abstractions/search.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ApiService } from "jslib-common/abstractions/api.service"; import { ModalService } from 'jslib-angular/services/modal.service';
import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { ValidationService } from 'jslib-angular/services/validation.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 { ModalService } from "jslib-angular/services/modal.service"; import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
import { ValidationService } from "jslib-angular/services/validation.service"; import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType"; import { SearchPipe } from 'jslib-angular/pipes/search.pipe';
import { ProviderUserType } from "jslib-common/enums/providerUserType"; import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
import { SearchPipe } from "jslib-angular/pipes/search.pipe"; import { ListResponse } from 'jslib-common/models/response/listResponse';
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe"; import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse';
import { ListResponse } from "jslib-common/models/response/listResponse"; import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { ProviderUserUserDetailsResponse } from "jslib-common/models/response/provider/providerUserResponse"; import { ProviderUserConfirmRequest } from 'jslib-common/models/request/provider/providerUserConfirmRequest';
import { ProviderUserBulkResponse } from 'jslib-common/models/response/provider/providerUserBulkResponse';
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest"; import { BasePeopleComponent } from 'src/app/common/base.people.component';
import { ProviderUserConfirmRequest } from "jslib-common/models/request/provider/providerUserConfirmRequest"; import { BulkStatusComponent } from 'src/app/organizations/manage/bulk/bulk-status.component';
import { ProviderUserBulkResponse } from "jslib-common/models/response/provider/providerUserBulkResponse"; import { EntityEventsComponent } from 'src/app/organizations/manage/entity-events.component';
import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
import { BasePeopleComponent } from "src/app/common/base.people.component"; import { BulkRemoveComponent } from './bulk/bulk-remove.component';
import { BulkStatusComponent } from "src/app/organizations/manage/bulk/bulk-status.component"; import { UserAddEditComponent } from './user-add-edit.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("eventsTemplate", { read: ViewContainerRef, static: true })
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; @ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
userStatusType = ProviderUserStatusType; @ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
providerId: string; @ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
accessEvents = false; @ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef;
@ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef;
@ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef;
constructor( userType = ProviderUserType;
apiService: ApiService, userStatusType = ProviderUserStatusType;
private route: ActivatedRoute, providerId: string;
i18nService: I18nService, accessEvents = false;
modalService: ModalService,
platformUtilsService: PlatformUtilsService,
cryptoService: CryptoService,
private router: Router,
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() { constructor(apiService: ApiService, private route: ActivatedRoute,
this.route.parent.params.subscribe(async (params) => { i18nService: I18nService, modalService: ModalService,
this.providerId = params.providerId; platformUtilsService: PlatformUtilsService, toasterService: ToasterService,
const provider = await this.providerService.get(this.providerId); cryptoService: CryptoService, private userService: UserService, private router: Router,
storageService: StorageService, searchService: SearchService, validationService: ValidationService,
logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe) {
super(apiService, searchService, i18nService, platformUtilsService, toasterService, cryptoService,
storageService, validationService, modalService, logService, searchPipe, userNamePipe);
}
if (!provider.canManageUsers) { ngOnInit() {
this.router.navigate(["../"], { relativeTo: this.route }); this.route.parent.params.subscribe(async params => {
return; this.providerId = params.providerId;
} const provider = await this.userService.getProvider(this.providerId);
this.accessEvents = provider.useEvents; if (!provider.canManageUsers) {
this.router.navigate(['../'], { relativeTo: this.route });
return;
}
await this.load(); this.accessEvents = provider.useEvents;
this.route.queryParams.pipe(first()).subscribe(async (qParams) => { await this.load();
this.searchText = qParams.search;
if (qParams.viewEvents != null) { const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
const user = this.users.filter((u) => u.id === qParams.viewEvents); this.searchText = qParams.search;
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) { if (qParams.viewEvents != null) {
this.events(user[0]); const user = this.users.filter(u => u.id === qParams.viewEvents);
} if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
this.events(user[0]);
}
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
});
}
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
return this.apiService.getProviderUsers(this.providerId);
}
deleteUser(id: string): Promise<any> {
return this.apiService.deleteProviderUser(this.providerId, id);
}
reinviteUser(id: string): Promise<any> {
return this.apiService.postProviderUserReinvite(this.providerId, id);
}
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
const request = new ProviderUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
}
async edit(user: ProviderUserUserDetailsResponse) {
const [modal] = await this.modalService.openViewRef(UserAddEditComponent, this.addEditModalRef, comp => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.providerUserId = user != null ? user.id : null;
comp.onSavedUser.subscribe(() => {
modal.close();
this.load();
});
comp.onDeletedUser.subscribe(() => {
modal.close();
this.removeUser(user);
});
});
}
async events(user: ProviderUserUserDetailsResponse) {
const [modal] = await this.modalService.openViewRef(EntityEventsComponent, this.eventsModalRef, comp => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.entityId = user.id;
comp.showUser = false;
comp.entity = 'user';
});
}
async bulkRemove() {
if (this.actionPromise != null) {
return;
} }
});
});
}
getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> { const [modal] = await this.modalService.openViewRef(BulkRemoveComponent, this.bulkRemoveModalRef, comp => {
return this.apiService.getProviderUsers(this.providerId); comp.providerId = this.providerId;
} comp.users = this.getCheckedUsers();
deleteUser(id: string): Promise<any> {
return this.apiService.deleteProviderUser(this.providerId, id);
}
reinviteUser(id: string): Promise<any> {
return this.apiService.postProviderUserReinvite(this.providerId, id);
}
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
const request = new ProviderUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
}
async edit(user: ProviderUserUserDetailsResponse) {
const [modal] = await this.modalService.openViewRef(
UserAddEditComponent,
this.addEditModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.providerUserId = user != null ? user.id : null;
comp.onSavedUser.subscribe(() => {
modal.close();
this.load();
}); });
comp.onDeletedUser.subscribe(() => {
modal.close(); await modal.onClosedPromise();
this.removeUser(user); await this.load();
}
async bulkReinvite() {
if (this.actionPromise != null) {
return;
}
const users = this.getCheckedUsers();
const filteredUsers = users.filter(u => u.status === ProviderUserStatusType.Invited);
if (filteredUsers.length <= 0) {
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('noSelectedUsersApplicable'));
return;
}
try {
const request = new ProviderUserBulkRequest(filteredUsers.map(user => user.id));
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
this.showBulkStatus(users, filteredUsers, response, this.i18nService.t('bulkReinviteMessage'));
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async bulkConfirm() {
if (this.actionPromise != null) {
return;
}
const [modal] = await this.modalService.openViewRef(BulkConfirmComponent, this.bulkConfirmModalRef, comp => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
}); });
}
);
}
async events(user: ProviderUserUserDetailsResponse) { await modal.onClosedPromise();
const [modal] = await this.modalService.openViewRef( await this.load();
EntityEventsComponent,
this.eventsModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.entityId = user.id;
comp.showUser = false;
comp.entity = "user";
}
);
}
async bulkRemove() {
if (this.actionPromise != null) {
return;
} }
const [modal] = await this.modalService.openViewRef( private async showBulkStatus(users: ProviderUserUserDetailsResponse[], filteredUsers: ProviderUserUserDetailsResponse[],
BulkRemoveComponent, request: Promise<ListResponse<ProviderUserBulkResponse>>, successfullMessage: string) {
this.bulkRemoveModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
}
);
await modal.onClosedPromise(); const [modal, childComponent] = await this.modalService.openViewRef(BulkStatusComponent, this.bulkStatusModalRef, comp => {
await this.load(); comp.loading = true;
}
async bulkReinvite() {
if (this.actionPromise != null) {
return;
}
const users = this.getCheckedUsers();
const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited);
if (filteredUsers.length <= 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("noSelectedUsersApplicable")
);
return;
}
try {
const request = new ProviderUserBulkRequest(filteredUsers.map((user) => user.id));
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
this.showBulkStatus(
users,
filteredUsers,
response,
this.i18nService.t("bulkReinviteMessage")
);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async bulkConfirm() {
if (this.actionPromise != null) {
return;
}
const [modal] = await this.modalService.openViewRef(
BulkConfirmComponent,
this.bulkConfirmModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
}
);
await modal.onClosedPromise();
await this.load();
}
private async showBulkStatus(
users: ProviderUserUserDetailsResponse[],
filteredUsers: ProviderUserUserDetailsResponse[],
request: Promise<ListResponse<ProviderUserBulkResponse>>,
successfullMessage: string
) {
const [modal, childComponent] = await this.modalService.openViewRef(
BulkStatusComponent,
this.bulkStatusModalRef,
(comp) => {
comp.loading = true;
}
);
// Workaround to handle closing the modal shortly after it has been opened
let close = false;
modal.onShown.subscribe(() => {
if (close) {
modal.close();
}
});
try {
const response = await request;
if (modal) {
const keyedErrors: any = response.data
.filter((r) => r.error !== "")
.reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
childComponent.users = users.map((user) => {
let message = keyedErrors[user.id] ?? successfullMessage;
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
message = this.i18nService.t("bulkFilteredMessage");
}
return {
user: user,
error: keyedErrors.hasOwnProperty(user.id),
message: message,
};
}); });
childComponent.loading = false;
} // Workaround to handle closing the modal shortly after it has been opened
} catch { let close = false;
close = true; modal.onShown.subscribe(() => {
modal.close(); if (close) {
modal.close();
}
});
try {
const response = await request;
if (modal) {
const keyedErrors: any = response.data.filter(r => r.error !== '').reduce((a, x) => ({ ...a, [x.id]: x.error }), {});
const keyedFilteredUsers: any = filteredUsers.reduce((a, x) => ({ ...a, [x.id]: x }), {});
childComponent.users = users.map(user => {
let message = keyedErrors[user.id] ?? successfullMessage;
if (!keyedFilteredUsers.hasOwnProperty(user.id)) {
message = this.i18nService.t('bulkFilteredMessage');
}
return {
user: user,
error: keyedErrors.hasOwnProperty(user.id),
message: message,
};
});
childComponent.loading = false;
}
} catch {
close = true;
modal.close();
}
} }
}
} }

View File

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

View File

@@ -1,121 +1,104 @@
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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderUserInviteRequest } from "jslib-common/models/request/provider/providerUserInviteRequest"; 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 { PermissionsApi } from "jslib-common/models/api/permissionsApi"; import { ProviderUserInviteRequest } from 'jslib-common/models/request/provider/providerUserInviteRequest';
import { ProviderUserType } from "jslib-common/enums/providerUserType"; import { PermissionsApi } from 'jslib-common/models/api/permissionsApi';
import { ProviderUserUpdateRequest } from "jslib-common/models/request/provider/providerUserUpdateRequest";
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;
@Input() providerUserId: string; @Input() providerUserId: string;
@Input() providerId: string; @Input() providerId: string;
@Output() onSavedUser = new EventEmitter(); @Output() onSavedUser = new EventEmitter();
@Output() onDeletedUser = new EventEmitter(); @Output() onDeletedUser = new EventEmitter();
loading = true; loading = true;
editMode: boolean = 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 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;
} catch (e) { } catch { }
this.logService.error(e); } else {
} this.title = this.i18nService.t('inviteUser');
} else { }
this.title = this.i18nService.t("inviteUser");
this.loading = false;
} }
this.loading = false; async submit() {
} try {
if (this.editMode) {
async submit() { const request = new ProviderUserUpdateRequest();
try { request.type = this.type;
if (this.editMode) { this.formPromise = this.apiService.putProviderUser(this.providerId, this.providerUserId, request);
const request = new ProviderUserUpdateRequest(); } else {
request.type = this.type; const request = new ProviderUserInviteRequest();
this.formPromise = this.apiService.putProviderUser( request.emails = this.emails.trim().split(/\s*,\s*/);
this.providerId, request.type = this.type;
this.providerUserId, this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
request }
); await this.formPromise;
} else { this.toasterService.popAsync('success', null,
const request = new ProviderUserInviteRequest(); this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
request.emails = this.emails.trim().split(/\s*,\s*/); this.onSavedUser.emit();
request.type = this.type; } catch { }
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
}
await this.formPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name)
);
this.onSavedUser.emit();
} catch (e) {
this.logService.error(e);
}
}
async delete() {
if (!this.editMode) {
return;
} }
const confirmed = await this.platformUtilsService.showDialog( async delete() {
this.i18nService.t("removeUserConfirmation"), if (!this.editMode) {
this.name, return;
this.i18nService.t("yes"), }
this.i18nService.t("no"),
"warning" const confirmed = await this.platformUtilsService.showDialog(
); this.i18nService.t('removeUserConfirmation'), this.name,
if (!confirmed) { this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
return false; if (!confirmed) {
return false;
}
try {
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
await this.deletePromise;
this.toasterService.popAsync('success', null, this.i18nService.t('removedUserId', this.name));
this.onDeletedUser.emit();
} catch { }
} }
try {
this.deletePromise = this.apiService.deleteProviderUser(this.providerId, this.providerUserId);
await this.deletePromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("removedUserId", this.name)
);
this.onDeletedUser.emit();
} catch (e) {
this.logService.error(e);
}
}
} }

View File

@@ -1,44 +1,44 @@
<app-navbar></app-navbar> <app-navbar></app-navbar>
<div class="org-nav" *ngIf="provider"> <div class="org-nav" *ngIf="provider">
<div class="container d-flex"> <div class="container d-flex">
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<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 class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
<div class="card-body py-2">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
{{'providerIsDisabled' | i18n}}
</div>
</div>
</div>
<ul class="nav nav-tabs" *ngIf="showMenuBar">
<li class="nav-item">
<a class="nav-link" routerLink="clients" routerLinkActive="active">
<i class="fa fa-university" aria-hidden="true"></i>
{{'clients' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="showManageTab">
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
<i class="fa fa-sliders" aria-hidden="true"></i>
{{'manage' | i18n}}
</a>
</li>
<li class="nav-item" *ngIf="showSettingsTab">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="fa fa-cogs" aria-hidden="true"></i>
{{'settings' | i18n}}
</a>
</li>
</ul>
</div> </div>
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!provider.enabled">
<div class="card-body py-2">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
{{ "providerIsDisabled" | i18n }}
</div>
</div>
</div>
<ul class="nav nav-tabs" *ngIf="showMenuBar">
<li class="nav-item">
<a class="nav-link" routerLink="clients" routerLinkActive="active">
<i class="fa fa-university" aria-hidden="true"></i>
{{ "clients" | i18n }}
</a>
</li>
<li class="nav-item" *ngIf="showManageTab">
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
<i class="fa fa-sliders" aria-hidden="true"></i>
{{ "manage" | i18n }}
</a>
</li>
<li class="nav-item" *ngIf="showSettingsTab">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<i class="fa fa-cogs" aria-hidden="true"></i>
{{ "settings" | i18n }}
</a>
</li>
</ul>
</div> </div>
</div>
</div> </div>
<div class="container page-content"> <div class="container page-content">
<router-outlet></router-outlet> <router-outlet></router-outlet>
</div> </div>
<app-footer></app-footer> <app-footer></app-footer>

View File

@@ -1,50 +1,51 @@
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;
private providerId: string;
constructor(private route: ActivatedRoute, private providerService: ProviderService) {} provider: Provider;
private providerId: string;
ngOnInit() { constructor(private route: ActivatedRoute, private userService: UserService) { }
document.body.classList.remove("layout_frontend");
this.route.params.subscribe(async (params) => {
this.providerId = params.providerId;
await this.load();
});
}
async load() { ngOnInit() {
this.provider = await this.providerService.get(this.providerId); document.body.classList.remove('layout_frontend');
} this.route.params.subscribe(async params => {
this.providerId = params.providerId;
get showMenuBar() { await this.load();
return this.showManageTab || this.showSettingsTab; });
} }
get showManageTab() { async load() {
return this.provider.canManageUsers || this.provider.canAccessEventLogs; this.provider = await this.userService.getProvider(this.providerId);
} }
get showSettingsTab() { get showMenuBar() {
return this.provider.isProviderAdmin; return this.showManageTab || this.showSettingsTab;
} }
get manageRoute(): string { get showManageTab() {
switch (true) { return this.provider.canManageUsers || this.provider.canAccessEventLogs;
case this.provider.canManageUsers: }
return "manage/people";
case this.provider.canAccessEventLogs: get showSettingsTab() {
return "manage/events"; return this.provider.isProviderAdmin;
}
get manageRoute(): string {
switch (true) {
case this.provider.canManageUsers:
return 'manage/people';
case this.provider.canAccessEventLogs:
return 'manage/events';
}
} }
}
} }

View File

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

View File

@@ -1,63 +1,69 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from '@angular/common';
import { ComponentFactoryResolver } from "@angular/core"; import { ComponentFactoryResolver } from '@angular/core';
import { NgModule } from "@angular/core"; import { NgModule } from '@angular/core';
import { FormsModule } from "@angular/forms"; import { FormsModule } from '@angular/forms';
import { ModalService } from "jslib-angular/services/modal.service"; import { ModalService } from 'jslib-angular/services/modal.service';
import { ProviderGuardService } from "./services/provider-guard.service"; import { ProviderGuardService } from './services/provider-guard.service';
import { ProviderTypeGuardService } from "./services/provider-type-guard.service"; import { ProviderTypeGuardService } from './services/provider-type-guard.service';
import { WebProviderService } from "./services/webProvider.service"; import { ProviderService } from './services/provider.service';
import { ProvidersLayoutComponent } from "./providers-layout.component"; import { ProvidersLayoutComponent } from './providers-layout.component';
import { ProvidersRoutingModule } from "./providers-routing.module"; import { ProvidersRoutingModule } from './providers-routing.module';
import { AddOrganizationComponent } from "./clients/add-organization.component"; import { AddOrganizationComponent } from './clients/add-organization.component';
import { ClientsComponent } from "./clients/clients.component"; import { ClientsComponent } from './clients/clients.component';
import { CreateOrganizationComponent } from "./clients/create-organization.component"; import { CreateOrganizationComponent } from './clients/create-organization.component';
import { AcceptProviderComponent } from "./manage/accept-provider.component"; import { AcceptProviderComponent } from './manage/accept-provider.component';
import { BulkConfirmComponent } from "./manage/bulk/bulk-confirm.component"; import { BulkConfirmComponent } from './manage/bulk/bulk-confirm.component';
import { BulkRemoveComponent } from "./manage/bulk/bulk-remove.component"; import { BulkRemoveComponent } from './manage/bulk/bulk-remove.component';
import { EventsComponent } from "./manage/events.component"; import { EventsComponent } from './manage/events.component';
import { ManageComponent } from "./manage/manage.component"; import { ManageComponent } from './manage/manage.component';
import { PeopleComponent } from "./manage/people.component"; import { PeopleComponent } from './manage/people.component';
import { UserAddEditComponent } from "./manage/user-add-edit.component"; import { UserAddEditComponent } from './manage/user-add-edit.component';
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 { SetupProviderComponent } from './setup/setup-provider.component';
import { SetupComponent } from "./setup/setup.component"; import { SetupComponent } from './setup/setup.component';
import { OssModule } from "src/app/oss.module"; import { OssModule } from 'src/app/oss.module';
@NgModule({ @NgModule({
imports: [CommonModule, FormsModule, OssModule, ProvidersRoutingModule], imports: [
declarations: [ CommonModule,
AcceptProviderComponent, FormsModule,
AccountComponent, OssModule,
AddOrganizationComponent, ProvidersRoutingModule,
BulkConfirmComponent, ],
BulkRemoveComponent, declarations: [
ClientsComponent, AcceptProviderComponent,
CreateOrganizationComponent, AccountComponent,
EventsComponent, AddOrganizationComponent,
ManageComponent, BulkConfirmComponent,
PeopleComponent, BulkRemoveComponent,
ProvidersLayoutComponent, ClientsComponent,
SettingsComponent, CreateOrganizationComponent,
SetupComponent, EventsComponent,
SetupProviderComponent, ManageComponent,
UserAddEditComponent, PeopleComponent,
], ProvidersLayoutComponent,
providers: [WebProviderService, ProviderGuardService, ProviderTypeGuardService], SettingsComponent,
SetupComponent,
SetupProviderComponent,
UserAddEditComponent,
],
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

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

View File

@@ -1,27 +1,31 @@
import { Injectable } from "@angular/core"; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router"; import {
ActivatedRouteSnapshot,
CanActivate,
Router,
} from '@angular/router';
import { ProviderService } from "jslib-common/abstractions/provider.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { Permissions } from "jslib-common/enums/permissions"; import { Permissions } from 'jslib-common/enums/permissions';
@Injectable() @Injectable()
export class ProviderTypeGuardService implements CanActivate { export class ProviderTypeGuardService implements CanActivate {
constructor(private providerService: ProviderService, private router: Router) {} constructor(private userService: UserService, private router: Router) { }
async canActivate(route: ActivatedRouteSnapshot) { async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.providerService.get(route.params.providerId); const provider = await this.userService.getProvider(route.params.providerId);
const permissions = route.data == null ? null : (route.data.permissions as Permissions[]); const permissions = route.data == null ? null : route.data.permissions as Permissions[];
if ( if (
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) || (permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) ||
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) || (permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) ||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers) (permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers)
) { ) {
return true; return true;
}
this.router.navigate(['/providers', provider.id]);
return false;
} }
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,36 +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,48 +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 class="fa fa-spinner fa-spin text-muted" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="sr-only">{{'loading' | i18n}}</span>
</div> </div>
<form <form *ngIf="provider && !loading" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
*ngIf="provider && !loading" <div class="row">
#form <div class="col-6">
(ngSubmit)="submit()" <div class="form-group">
[appApiAction]="formPromise" <label for="name">{{'providerName' | i18n}}</label>
ngNativeValidate <input id="name" class="form-control" type="text" name="Name" [(ngModel)]="provider.name"
> [disabled]="selfHosted">
<div class="row"> </div>
<div class="col-6"> <div class="form-group">
<div class="form-group"> <label for="billingEmail">{{'billingEmail' | i18n}}</label>
<label for="name">{{ "providerName" | i18n }}</label> <input id="billingEmail" class="form-control" type="text" name="BillingEmail"
<input [(ngModel)]="provider.billingEmail" [disabled]="selfHosted">
id="name" </div>
class="form-control" </div>
type="text" <div class="col-6">
name="Name" <app-avatar data="{{provider.name}}" dynamic="true" size="75" fontSize="35"></app-avatar>
[(ngModel)]="provider.name" </div>
[disabled]="selfHosted"
/>
</div>
<div class="form-group">
<label for="billingEmail">{{ "billingEmail" | i18n }}</label>
<input
id="billingEmail"
class="form-control"
type="text"
name="BillingEmail"
[(ngModel)]="provider.billingEmail"
[disabled]="selfHosted"
/>
</div>
</div> </div>
<div class="col-6"> <button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<app-avatar data="{{ provider.name }}" dynamic="true" size="75" fontSize="35"></app-avatar> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</div> <span>{{'save' | i18n}}</span>
</div> </button>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
</form> </form>

View File

@@ -1,65 +1,62 @@
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 { ProviderUpdateRequest } from 'jslib-common/models/request/provider/providerUpdateRequest';
import { ProviderResponse } from "jslib-common/models/response/provider/providerResponse"; 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;
loading = true; loading = true;
provider: ProviderResponse; provider: ProviderResponse;
formPromise: Promise<any>; formPromise: Promise<any>;
taxFormPromise: Promise<any>; taxFormPromise: Promise<any>;
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);
} catch (e) { } catch (e) {
this.logService.error(`Handled exception: ${e}`); this.logService.error(`Handled exception: ${e}`);
} }
}); });
this.loading = false; this.loading = false;
} }
async submit() { async submit() {
try { try {
const request = new ProviderUpdateRequest(); const request = new ProviderUpdateRequest();
request.name = this.provider.name; request.name = this.provider.name;
request.businessName = this.provider.businessName; request.businessName = this.provider.businessName;
request.billingEmail = this.provider.billingEmail; request.billingEmail = this.provider.billingEmail;
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => { this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
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

@@ -1,17 +1,17 @@
<div class="container page-content"> <div class="container page-content">
<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 class="col-9">
<router-outlet></router-outlet>
</div> </div>
</div>
</div> </div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div> </div>

View File

@@ -1,23 +1,20 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from '@angular/router';
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';
@Component({ @Component({
selector: "provider-settings", selector: 'provider-settings',
templateUrl: "settings.component.html", templateUrl: 'settings.component.html',
}) })
export class SettingsComponent { export class SettingsComponent {
constructor( constructor(private route: ActivatedRoute, private userService: UserService,
private route: ActivatedRoute, private platformUtilsService: PlatformUtilsService) { }
private providerService: ProviderService,
private platformUtilsService: PlatformUtilsService
) {}
ngOnInit() { ngOnInit() {
this.route.parent.params.subscribe(async (params) => { this.route.parent.params.subscribe(async params => {
const provider = await this.providerService.get(params.providerId); const provider = await this.userService.getProvider(params.providerId);
}); });
} }
} }

View File

@@ -1,31 +1,27 @@
<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="fa fa-spinner fa-spin fa-2x text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}" </p>
aria-hidden="true" </div>
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</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 routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block"> <a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{ "logIn" | i18n }} {{'logIn' | i18n}}
</a> </a>
</div> </div>
</div>
</div>
</div> </div>
</div>
</div> </div>
</div>
</div> </div>

View File

@@ -1,21 +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';
async authedHandler(qParams: any) { requiredParameters = ['providerId', 'email', 'token'];
this.router.navigate(["/providers/setup"], { queryParams: qParams });
}
// tslint:disable-next-line async authedHandler(qParams: any) {
async unauthedHandler(qParams: any) {} this.router.navigate(['/providers/setup'], {queryParams: qParams});
}
// tslint:disable-next-line
async unauthedHandler(qParams: any) {}
} }

View File

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

View File

@@ -1,101 +1,106 @@
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 {
Toast,
ToasterService,
} from 'angular2-toaster';
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 { ValidationService } from 'jslib-angular/services/validation.service';
import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { I18nService } from 'jslib-common/abstractions/i18n.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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.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;
authed = false; authed = false;
email: string; email: string;
formPromise: Promise<any>; formPromise: Promise<any>;
providerId: string; providerId: string;
token: string; token: string;
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) => { let fired = false;
const error = qParams.providerId == null || qParams.email == null || qParams.token == null; this.route.queryParams.subscribe(async qParams => {
if (fired) {
return;
}
fired = true;
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.toasterService.popAsync(toast);
); this.router.navigate(['/']);
this.router.navigate(["/"]); return;
return; }
}
this.providerId = qParams.providerId; this.providerId = qParams.providerId;
this.token = qParams.token; this.token = qParams.token;
// Check if provider exists, redirect if it does // Check if provider exists, redirect if it does
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(['/']);
} }
}); });
} }
async submit() { async submit() {
this.formPromise = this.doSubmit(); this.formPromise = this.doSubmit();
await this.formPromise; await this.formPromise;
this.formPromise = null; this.formPromise = null;
} }
async doSubmit() { async doSubmit() {
try { try {
const shareKey = await this.cryptoService.makeShareKey(); const shareKey = await this.cryptoService.makeShareKey();
const key = shareKey[0].encryptedString; const key = shareKey[0].encryptedString;
const request = new ProviderSetupRequest(); const request = new ProviderSetupRequest();
request.name = this.name; request.name = this.name;
request.billingEmail = this.billingEmail; request.billingEmail = this.billingEmail;
request.token = this.token; request.token = this.token;
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,8 +1,12 @@
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].entryModule = "bitwarden_license/src/app/app.module#AppModule"; webpackConfig.plugins[webpackConfig.plugins.length -1] = new AngularCompilerPlugin({
tsConfigPath: 'tsconfig.json',
entryModule: 'bitwarden_license/src/app/app.module#AppModule',
sourceMap: true,
});
module.exports = webpackConfig; module.exports = webpackConfig;

View File

@@ -1,53 +1,32 @@
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: { };
...require("./config/base.json").dev,
...loadConfig(envName).dev,
...loadConfig("local").dev,
},
};
} }
function log(configObj) { function log(configObj) {
const repeatNum = 50; const repeatNum = 50;
console.log(`${"=".repeat(repeatNum)}\nenvConfig`); console.log(`${"=".repeat(repeatNum)}\nenvConfig`);
console.log(JSON.stringify(configObj, null, 2)); console.log(JSON.stringify(configObj, null, 2));
console.log(`${"=".repeat(repeatNum)}`); console.log(`${"=".repeat(repeatNum)}`);
} }
function loadConfig(configName) { function loadConfig(configName) {
try { try {
return require(`./config/${configName}.json`); return require(`./config/${configName}.json`);
} 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 { }
throw e; else {
throw e;
}
} }
}
}
function generateSubstitutions(configObj) {
let result = {};
Object.keys(configObj.compileFlags ?? {}).forEach(key => {
result = { ...result, ...generateSubstitutionsForFlag(key) };
});
return result;
}
function generateSubstitutionsForFlag(flagName) {
return {
"featureFlag: (.*)[\\s\\S]*?\\/\\/ endFeatureFlag": ""
};
} }
module.exports = { module.exports = {
load, load,
log, log
generateSubstitutions,
}; };

View File

@@ -1,12 +1,9 @@
{ {
"urls": {}, "urls": {},
"stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD", "stripeKey": "pk_test_KPoCfZXu7mznb9uSCPZ2JpTD",
"braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2", "braintreeKey": "sandbox_r72q8jq6_9pnxkwm75f87sdc2",
"paypal": { "paypal": {
"businessId": "AD3LAUZSNVPJY", "businessId": "AD3LAUZSNVPJY",
"buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr" "buttonAction": "https://www.sandbox.paypal.com/cgi-bin/webscr"
}, }
"dev": {
"allowedHosts": "auto"
}
} }

View File

@@ -1,17 +1,12 @@
{ {
"urls": { "urls": {
"icons": "https://icons.bitwarden.net", "icons": "https://icons.bitwarden.net",
"notifications": "https://notifications.bitwarden.com" "notifications": "https://notifications.bitwarden.com"
}, },
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk", "stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd", "braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
"paypal": { "paypal": {
"businessId": "4ZDA7DLUUJGMN", "businessId": "4ZDA7DLUUJGMN",
"buttonAction": "https://www.paypal.com/cgi-bin/webscr" "buttonAction": "https://www.paypal.com/cgi-bin/webscr"
}, }
"dev": {
"proxyApi": "https://api.bitwarden.com",
"proxyIdentity": "https://identity.bitwarden.com",
"proxyEvents": "https://events.bitwarden.com"
}
} }

View File

@@ -1,14 +1,10 @@
{ {
"urls": {
"notifications": "http://localhost:61840"
},
"dev": {
"proxyApi": "http://localhost:4000", "proxyApi": "http://localhost:4000",
"proxyIdentity": "http://localhost:33656", "proxyIdentity": "http://localhost:33656",
"proxyEvents": "http://localhost:46273", "proxyEvents": "http://localhost:46273",
"proxyNotifications": "http://localhost:61840" "proxyNotifications": "http://localhost:61840",
}, "allowedHosts": [],
"compileFlags": { "urls": {
"Test": false "notifications": "http://localhost:61840"
} }
} }

View File

View File

@@ -1,11 +1,6 @@
{ {
"urls": { "urls": {
"icons": "https://icons.qa.bitwarden.pw", "icons": "https://icons.qa.bitwarden.pw",
"notifications": "https://notifications.qa.bitwarden.pw" "notifications": "https://notifications.qa.bitwarden.pw"
}, }
"dev": {
"proxyApi": "https://api.qa.bitwarden.pw",
"proxyIdentity": "https://identity.qa.bitwarden.pw",
"proxyEvents": "https://events.qa.bitwarden.pw"
}
} }

View File

@@ -1,9 +1,7 @@
project_id_env: _CROWDIN_PROJECT_ID project_id_env: _CROWDIN_PROJECT_ID
api_token_env: CROWDIN_API_TOKEN api_token_env: CROWDIN_API_TOKEN
preserve_hierarchy: true
files: files:
- source: /src/locales/en/messages.json - source: /src/locales/en/messages.json
dest: /src/locales/en/%file_name%.%file_extension%
translation: /src/locales/%two_letters_code%/%original_file_name% translation: /src/locales/%two_letters_code%/%original_file_name%
update_option: update_as_unapproved update_option: update_as_unapproved
languages_mapping: languages_mapping:

2
jslib

Submodule jslib updated: e4cd0af2f9...5fb0247a6a

13816
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "bitwarden-web", "name": "bitwarden-web",
"version": "2.25.1", "version": "2.24.3",
"license": "GPL-3.0", "license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web", "repository": "https://github.com/bitwarden/web",
"scripts": { "scripts": {
@@ -29,57 +29,44 @@
"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": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' && prettier --check .", "lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' || true",
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --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",
"buffer": "^6.0.3", "clean-webpack-plugin": "^3.0.0",
"clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^6.4.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",
"del": "^6.0.0",
"file-loader": "^6.2.0",
"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",
"prettier": "2.5.1",
"process": "^0.11.10",
"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",
"terser-webpack-plugin": "^5.2.5", "tapable": "^1.1.3",
"ts-loader": "^9.2.5", "terser-webpack-plugin": "^4.2.3",
"ts-loader": "^8.1.0",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"tslint-loader": "^3.5.4", "tslint-loader": "^3.5.4",
"typescript": "4.3.5", "typescript": "4.1.5",
"util": "^0.12.4", "webpack": "^4.46.0",
"webpack": "^5.64.4", "webpack-cli": "^4.6.0",
"webpack-cli": "^4.9.1", "webpack-dev-server": "^3.11.2"
"webpack-dev-server": "^4.6.0"
}, },
"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.30.1", "braintree-web-drop-in": "1.30.1",
"browser-hrtime": "^1.1.8", "browser-hrtime": "^1.1.8",
@@ -88,20 +75,14 @@
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.6.0", "jquery": "3.6.0",
"ngx-infinite-scroll": "^10.0.1", "ngx-infinite-scroll": "^10.0.1",
"ngx-toastr": "14.1.4",
"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"
}, },
"engines": { "engines": {
"node": "~16", "node": "~14",
"npm": "~8" "npm": "~7"
},
"lint-staged": {
"*": "prettier --ignore-unknown --write",
"*.png": "node scripts/optimize.js"
} }
} }

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,56 +1,50 @@
<!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-B0vP5xmATw1+K9KRQjQERJvTumQW0nPEzvF6L/Z6nronJ3oUOFUFpCjEUQouq2+l" <link href="/404/styles.css" rel="stylesheet" type="text/css">
/>
<link
href="/404/font-awesome.min.css"
rel="stylesheet"
type="text/css"
integrity="sha512-SfTiTlX6kk+qitfevl/7LibUOeJWlt9rbyDn92a1DqWOw9vWG2MFoays0sgObmWazO5BQPiFucnnEAjpAB+/Sw=="
/>
<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>
<div class="banner"> <div class="banner">
<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"><i class="fa fa-shield"></i>&nbsp; <strong>bit</strong>warden</div> <div class="col brand">
<i class="fa fa-shield"></i>&nbsp;
<strong>bit</strong>warden</span>
</div>
</div>
</div>
</div> </div>
</div> <div class="container inner content">
</div> <h2>Page not found!</h2>
<div class="container inner content"> <p>Sorry, but the page you were looking for could not be found.</p>
<h2>Page not found!</h2> <p>
<p>Sorry, but the page you were looking for could not be found.</p> <a href="/">
<p> <img src="/images/404.png" class="img-fluid" alt="404 image" width="80%"/>
<a href="/"> </a>
<img src="/images/404.png" class="img-fluid" alt="404 image" width="80%" /> </p>
</a> <p>You can <a href="/">return to the web vault</a>, check our <a href="https://status.bitwarden.com/">status page</a>
</p> or <a href="https://bitwarden.com/contact/">contact us</a>.</p>
<p> </div>
You can <a href="/">return to the web vault</a>, check our <div class="container footer text-muted content">
<a href="https://status.bitwarden.com/">status page</a> or © Copyright 2021 Bitwarden, Inc.
<a href="https://bitwarden.com/contact/">contact us</a>. </div>
</p> </body>
</div>
<div class="container footer text-muted content">© Copyright 2022 Bitwarden, Inc.</div>
</body>
</html> </html>

View File

@@ -1,121 +1,119 @@
@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, height: 100%;
.row { -webkit-font-smoothing: antialiased;
height: 100%;
-webkit-font-smoothing: antialiased;
} }
h2 { h2 {
font-size: 25px; font-size: 25px;
margin-bottom: 12.5px; margin-bottom: 12.5px;
font-weight: 500; font-weight: 500;
line-height: 1.1; line-height: 1.1;
} }
.brand { .brand {
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;
} }
.content { .content {
padding-top: 20px; padding-top: 20px;
padding-bottom: 20px; padding-bottom: 20px;
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
} }
.footer { .footer {
padding: 40px 0 40px 0; padding: 40px 0 40px 0;
border-top: 1px solid #dee2e6; border-top: 1px solid #dee2e6;
} }

View File

@@ -1,41 +1,34 @@
<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="fa fa-spinner fa-spin fa-2x text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}" </p>
aria-hidden="true" </div>
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</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 routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block"> <a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{ "logIn" | i18n }} {{'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" </a>
> </div>
{{ "createAccount" | i18n }} </div>
</a> </div>
</div>
</div> </div>
</div>
</div> </div>
</div>
</div> </div>

View File

@@ -1,54 +1,60 @@
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;
protected requiredParameters: string[] = ["id", "name", "email", "token"]; name: string;
protected failedShortMessage = "emergencyInviteAcceptFailedShort";
protected failedMessage = "emergencyInviteAcceptFailed";
constructor( protected requiredParameters: string[] = ['id', 'name', 'email', 'token'];
router: Router, protected failedShortMessage = 'emergencyInviteAcceptFailedShort';
platformUtilsService: PlatformUtilsService, protected failedMessage = 'emergencyInviteAcceptFailed';
i18nService: I18nService,
route: ActivatedRoute,
private apiService: ApiService,
stateService: StateService
) {
super(router, platformUtilsService, i18nService, route, stateService);
}
async authedHandler(qParams: any): Promise<void> { constructor(router: Router, toasterService: ToasterService,
const request = new EmergencyAccessAcceptRequest(); i18nService: I18nService, route: ActivatedRoute,
request.token = qParams.token; private apiService: ApiService, userService: UserService,
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request); stateService: StateService) {
await this.actionPromise; super(router, toasterService, i18nService, route, userService, stateService);
this.platformUtilService.showToast( }
"success",
this.i18nService.t("inviteAccepted"), async authedHandler(qParams: any): Promise<void> {
this.i18nService.t("emergencyInviteAcceptedDesc"), const request = new EmergencyAccessAcceptRequest();
{ timeout: 10000 } request.token = qParams.token;
); this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
this.router.navigate(["/vault"]); await this.actionPromise;
} const toast: Toast = {
type: 'success',
async unauthedHandler(qParams: any): Promise<void> { title: this.i18nService.t('inviteAccepted'),
this.name = qParams.name; body: this.i18nService.t('emergencyInviteAcceptedDesc'),
if (this.name != null) { timeout: 10000,
// Fix URL encoding of space issue with Angular };
this.name = this.name.replace(/\+/g, " "); this.toasterService.popAsync(toast);
this.router.navigate(['/vault']);
}
async unauthedHandler(qParams: any): Promise<void> {
this.name = qParams.name;
if (this.name != null) {
// Fix URL encoding of space issue with Angular
this.name = this.name.replace(/\+/g, ' ');
}
} }
}
} }

View File

@@ -1,42 +1,35 @@
<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="fa fa-spinner fa-spin fa-2x text-muted" <span class="sr-only">{{'loading' | i18n}}</span>
title="{{ 'loading' | i18n }}" </p>
aria-hidden="true" </div>
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</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 routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block"> <a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{ "logIn" | i18n }} {{'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" </a>
> </div>
{{ "createAccount" | i18n }} </div>
</a> </div>
</div>
</div> </div>
</div>
</div> </div>
</div>
</div> </div>

View File

@@ -1,127 +1,114 @@
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 { OrganizationUserAcceptRequest } from "jslib-common/models/request/organizationUserAcceptRequest"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest"; import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.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 { Utils } from "jslib-common/misc/utils"; import { OrganizationUserAcceptRequest } from 'jslib-common/models/request/organizationUserAcceptRequest';
import { Policy } from "jslib-common/models/domain/policy"; import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest';
import { BaseAcceptComponent } from "../common/base.accept.component";
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 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> {
const request = new OrganizationUserAcceptRequest();
request.token = qParams.token;
if (await this.performResetPasswordAutoEnroll(qParams)) {
this.actionPromise = this.apiService
.postOrganizationUserAccept(qParams.organizationId, qParams.organizationUserId, request)
.then(() => {
// Retrieve Public Key
return this.apiService.getOrganizationKeys(qParams.organizationId);
})
.then(async (response) => {
if (response == null) {
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
}
const publicKey = Utils.fromB64ToArray(response.publicKey);
// RSA Encrypt user's encKey.key with organization public key
const encKey = await this.cryptoService.getEncKey();
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
// Create request and execute enrollment
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
return this.apiService.putOrganizationUserResetPasswordEnrollment(
qParams.organizationId,
await this.stateService.getUserId(),
resetRequest
);
});
} else {
this.actionPromise = this.apiService.postOrganizationUserAccept(
qParams.organizationId,
qParams.organizationUserId,
request
);
} }
await this.actionPromise; async authedHandler(qParams: any): Promise<void> {
this.platformUtilService.showToast( const request = new OrganizationUserAcceptRequest();
"success", request.token = qParams.token;
this.i18nService.t("inviteAccepted"), if (await this.performResetPasswordAutoEnroll(qParams)) {
this.i18nService.t("inviteAcceptedDesc"), this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
{ timeout: 10000 } qParams.organizationUserId, request).then(() => {
); // Retrieve Public Key
return this.apiService.getOrganizationKeys(qParams.organizationId);
}).then(async response => {
if (response == null) {
throw new Error(this.i18nService.t('resetPasswordOrgKeysError'));
}
await this.stateService.setOrganizationInvitation(null); const publicKey = Utils.fromB64ToArray(response.publicKey);
this.router.navigate(["/vault"]);
}
async unauthedHandler(qParams: any): Promise<void> { // RSA Encrypt user's encKey.key with organization public key
this.orgName = qParams.organizationName; const encKey = await this.cryptoService.getEncKey();
if (this.orgName != null) { const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer);
// Fix URL encoding of space issue with Angular
this.orgName = this.orgName.replace(/\+/g, " ");
}
await this.stateService.setOrganizationInvitation(qParams);
}
private async performResetPasswordAutoEnroll(qParams: any): Promise<boolean> { // Create request and execute enrollment
let policyList: Policy[] = null; const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
try { resetRequest.resetPasswordKey = encryptedKey.encryptedString;
const policies = await this.apiService.getPoliciesByToken(
qParams.organizationId, // Get User Id
qParams.token, const userId = await this.userService.getUserId();
qParams.email,
qParams.organizationUserId return this.apiService.putOrganizationUserResetPasswordEnrollment(qParams.organizationId, userId, resetRequest);
); });
policyList = this.policyService.mapPoliciesFromToken(policies); } else {
} catch (e) { this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
this.logService.error(e); qParams.organizationUserId, request);
}
await this.actionPromise;
const toast: Toast = {
type: 'success',
title: this.i18nService.t('inviteAccepted'),
body: this.i18nService.t('inviteAcceptedDesc'),
timeout: 10000,
};
this.toasterService.popAsync(toast);
await this.stateService.remove('orgInvitation');
this.router.navigate(['/vault']);
} }
if (policyList != null) { async unauthedHandler(qParams: any): Promise<void> {
const result = this.policyService.getResetPasswordPolicyOptions( this.orgName = qParams.organizationName;
policyList, if (this.orgName != null) {
qParams.organizationId // Fix URL encoding of space issue with Angular
); this.orgName = this.orgName.replace(/\+/g, ' ');
// Return true if policy enabled and auto-enroll enabled }
return result[1] && result[0].autoEnrollEnabled; await this.stateService.save('orgInvitation', qParams);
} }
return false; private async performResetPasswordAutoEnroll(qParams: any): Promise<boolean> {
} let policyList: Policy[] = null;
try {
const policies = await this.apiService.getPoliciesByToken(qParams.organizationId, qParams.token,
qParams.email, qParams.organizationUserId);
policyList = this.policyService.mapPoliciesFromToken(policies);
} catch { }
if (policyList != null) {
const result = this.policyService.getResetPasswordPolicyOptions(policyList, qParams.organizationId);
// Return true if policy enabled and auto-enroll enabled
return result[1] && result[0].autoEnrollEnabled;
}
return false;
}
} }

View File

@@ -1,44 +1,27 @@
<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" </div>
name="Email" <hr>
[(ngModel)]="email" <div class="d-flex">
required <button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
appAutofocus <span [hidden]="form.loading">{{'submit' | i18n}}</span>
inputmode="email" <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
appInputVerbatim="false" </button>
/> <a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
<small class="form-text text-muted">{{ "enterEmailToGetHint" | i18n }}</small> {{'cancel' | i18n}}
</div> </a>
<hr /> </div>
<div class="d-flex"> </div>
<button </div>
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span [hidden]="form.loading">{{ "submit" | i18n }}</span>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
</div> </div>
</div>
</div> </div>
</div>
</form> </form>

View File

@@ -1,25 +1,19 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { Router } from "@angular/router"; import { Router } from '@angular/router';
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 { 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"; 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, super(router, i18nService, apiService, platformUtilsService);
apiService: ApiService, }
platformUtilsService: PlatformUtilsService,
logService: LogService
) {
super(router, i18nService, apiService, platformUtilsService, logService);
}
} }

View File

@@ -1,68 +1,42 @@
<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="text-center mb-4"> <p class="text-center mb-4">
<i class="fa fa-lock fa-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 </button>
appInputVerbatim </div>
/> <small class="text-muted form-text">
<button {{'loggedInAsEmailOn' | i18n : email : webVaultHostname}}
type="button" </small>
class="ml-1 btn btn-link" </div>
appA11yTitle="{{ 'toggleVisibility' | i18n }}" <hr>
(click)="togglePassword()" <div class="d-flex">
> <button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<i <span>
class="fa fa-lg" <i class="fa fa-unlock-alt" aria-hidden="true"></i> {{'unlock' | i18n}}
aria-hidden="true" </span>
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }" <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
></i> </button>
</button> <button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
{{'logOut' | i18n}}
</button>
</div>
</div>
</div> </div>
<small class="text-muted form-text">
{{ "loggedInAsEmailOn" | i18n: email:webVaultHostname }}
</small>
</div>
<hr />
<div class="d-flex">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span>
<i class="fa fa-unlock-alt" aria-hidden="true"></i> {{ "unlock" | i18n }}
</span>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="btn btn-outline-secondary btn-block ml-2 mt-0"
(click)="logOut()"
>
{{ "logOut" | i18n }}
</button>
</div>
</div> </div>
</div>
</div> </div>
</div>
</form> </form>

View File

@@ -1,65 +1,44 @@
import { Component, NgZone } from "@angular/core"; import { Component } from '@angular/core';
import { Router } from "@angular/router"; import { Router } from '@angular/router';
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 { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { LogService } from "jslib-common/abstractions/log.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { MessagingService } from "jslib-common/abstractions/messaging.service"; import { StateService } from 'jslib-common/abstractions/state.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { StorageService } from 'jslib-common/abstractions/storage.service';
import { StateService } from "jslib-common/abstractions/state.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.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"; 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) {
vaultTimeoutService: VaultTimeoutService, super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
environmentService: EnvironmentService, storageService, vaultTimeoutService, environmentService, stateService, apiService);
private routerService: RouterService, }
stateService: StateService,
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.indexOf("lock") === -1) { if (previousUrl !== '/' && previousUrl.indexOf('lock') === -1) {
this.successRoute = previousUrl; this.successRoute = previousUrl;
} }
this.router.navigate([this.successRoute]); this.router.navigate([this.successRoute]);
}; };
} }
} }

View File

@@ -1,101 +1,61 @@
<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" </app-callout>
> <div class="form-group">
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }} <label for="email">{{'emailAddress' | i18n}}</label>
</app-callout> <input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
<div class="form-group"> inputmode="email" appInputVerbatim="false">
<label for="email">{{ "emailAddress" | i18n }}</label> </div>
<input <div class="form-group">
id="email" <label for="masterPassword">{{'masterPass' | i18n}}</label>
class="form-control" <div class="d-flex">
type="text" <input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="Email" name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
[(ngModel)]="email" required appInputVerbatim>
required <button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
inputmode="email" (click)="togglePassword()">
appInputVerbatim="false" <i class="fa fa-lg" aria-hidden="true"
/> [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</div> </button>
<div class="form-group"> </div>
<label for="masterPassword">{{ "masterPass" | i18n }}</label> <small class="form-text">
<div class="d-flex"> <a routerLink="/hint">{{'getMasterPasswordHint' | i18n}}</a>
<input </small>
id="masterPassword" </div>
type="{{ showPassword ? 'text' : 'password' }}" <div class="form-check mb-3">
name="MasterPassword" <input type="checkbox" class="form-check-input" id="rememberEmail" name="RememberEmail"
class="text-monospace form-control" [(ngModel)]="rememberEmail">
[(ngModel)]="masterPassword" <label class="form-check-label" for="rememberEmail">{{'rememberEmail' | i18n}}</label>
required </div>
appInputVerbatim <div class="mb-n3" [hidden]="!showCaptcha()"><iframe id="hcaptcha_iframe" height="80"></iframe></div>
/> <hr>
<button <div class="d-flex">
type="button" <button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
class="ml-1 btn btn-link" <span>
appA11yTitle="{{ 'toggleVisibility' | i18n }}" <i class="fa fa-sign-in" aria-hidden="true"></i> {{'logIn' | i18n}}
(click)="togglePassword()" </span>
> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<i </button>
class="fa fa-lg" <a routerLink="/register" [queryParams]="{email: email}"
aria-hidden="true" class="btn btn-outline-secondary btn-block ml-2 mt-0">
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }" <i class="fa fa-pencil-square-o" aria-hidden="true"></i> {{'createAccount' | i18n}}
></i> </a>
</button> </div>
<div class="d-flex">
<a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2">
<i class="fa fa-bank" aria-hidden="true"></i> {{'enterpriseSingleSignOn' | i18n}}
</a>
</div>
</div>
</div> </div>
<small class="form-text">
<a routerLink="/hint">{{ "getMasterPasswordHint" | i18n }}</a>
</small>
</div>
<div class="form-check mb-3">
<input
type="checkbox"
class="form-check-input"
id="rememberEmail"
name="RememberEmail"
[(ngModel)]="rememberEmail"
/>
<label class="form-check-label" for="rememberEmail">{{ "rememberEmail" | i18n }}</label>
</div>
<div class="mb-n3" [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<hr />
<div class="d-flex">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span> <i class="fa fa-sign-in" aria-hidden="true"></i> {{ "logIn" | i18n }} </span>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a
routerLink="/register"
[queryParams]="{ email: email }"
class="btn btn-outline-secondary btn-block ml-2 mt-0"
>
<i class="fa fa-pencil-square-o" aria-hidden="true"></i> {{ "createAccount" | i18n }}
</a>
</div>
<div class="d-flex">
<a routerLink="/sso" class="btn btn-outline-secondary btn-block mt-2">
<i class="fa fa-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
</a>
</div>
</div> </div>
</div>
</div> </div>
</div>
</form> </form>

View File

@@ -1,118 +1,87 @@
import { Component, NgZone } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute, Router } from "@angular/router"; import {
ActivatedRoute,
Router,
} from '@angular/router';
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 { 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 { ApiService } from "jslib-common/abstractions/api.service"; import { LoginComponent as BaseLoginComponent } from 'jslib-angular/components/login.component';
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 { LoginComponent as BaseLoginComponent } from "jslib-angular/components/login.component"; import { Policy } from 'jslib-common/models/domain/policy';
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;
constructor( showResetPasswordAutoEnrollWarning = false;
authService: AuthService,
router: Router,
i18nService: I18nService,
private route: ActivatedRoute,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService,
cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService,
private policyService: PolicyService,
logService: LogService,
ngZone: NgZone
) {
super(
authService,
router,
platformUtilsService,
i18nService,
stateService,
environmentService,
passwordGenerationService,
cryptoFunctionService,
logService,
ngZone
);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}
async ngOnInit() { constructor(authService: AuthService, router: Router,
this.route.queryParams.pipe(first()).subscribe(async (qParams) => { i18nService: I18nService, private route: ActivatedRoute,
if (qParams.email != null && qParams.email.indexOf("@") > -1) { storageService: StorageService, stateService: StateService,
this.email = qParams.email; platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
} passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService,
if (qParams.premium != null) { private apiService: ApiService, private policyService: PolicyService) {
this.stateService.setLoginRedirect({ route: "/settings/premium" }); super(authService, router,
} else if (qParams.org != null) { platformUtilsService, i18nService,
this.stateService.setLoginRedirect({ stateService, environmentService,
route: "/settings/create-organization", passwordGenerationService, cryptoFunctionService,
qParams: { plan: qParams.org }, storageService);
}); this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}
// Are they coming from an email for sponsoring a families organization
if (qParams.sponsorshipToken != null) {
// After logging in redirect them to setup the families sponsorship
this.stateService.setLoginRedirect({
route: "/setup/families-for-enterprise",
qParams: { token: qParams.sponsorshipToken },
});
}
await super.ngOnInit();
});
const invite = await this.stateService.getOrganizationInvitation();
if (invite != null) {
let policyList: Policy[] = null;
try {
const policies = await this.apiService.getPoliciesByToken(
invite.organizationId,
invite.token,
invite.email,
invite.organizationUserId
);
policyList = this.policyService.mapPoliciesFromToken(policies);
} catch (e) {
this.logService.error(e);
}
if (policyList != null) {
const result = this.policyService.getResetPasswordPolicyOptions(
policyList,
invite.organizationId
);
// Set to true if policy enabled and auto-enroll enabled
this.showResetPasswordAutoEnrollWarning = result[1] && result[0].autoEnrollEnabled;
}
} }
}
async goAfterLogIn() { async ngOnInit() {
const loginRedirect = await this.stateService.getLoginRedirect(); const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
if (loginRedirect != null) { if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams }); this.email = qParams.email;
await this.stateService.setLoginRedirect(null); }
} else { if (qParams.premium != null) {
this.router.navigate([this.successRoute]); this.stateService.save('loginRedirect', { route: '/settings/premium' });
} else if (qParams.org != null) {
this.stateService.save('loginRedirect',
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
}
await super.ngOnInit();
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
});
const invite = await this.stateService.get<any>('orgInvitation');
if (invite != null) {
let policyList: Policy[] = null;
try {
const policies = await this.apiService.getPoliciesByToken(invite.organizationId, invite.token,
invite.email, invite.organizationUserId);
policyList = this.policyService.mapPoliciesFromToken(policies);
} catch { }
if (policyList != null) {
const result = this.policyService.getResetPasswordPolicyOptions(policyList, invite.organizationId);
// Set to true if policy enabled and auto-enroll enabled
this.showResetPasswordAutoEnrollWarning = result[1] && result[0].autoEnrollEnabled;
}
}
}
async goAfterLogIn() {
const loginRedirect = await this.stateService.get<any>('loginRedirect');
if (loginRedirect != null) {
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
await this.stateService.remove('loginRedirect');
} else {
this.router.navigate([this.successRoute]);
}
} }
}
} }

View File

@@ -1,44 +1,27 @@
<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">{{ "deleteAccount" | i18n }}</p> <p class="lead text-center mb-4">{{'deleteAccount' | i18n}}</p>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<p>{{ "deleteRecoverDesc" | i18n }}</p> <p>{{'deleteRecoverDesc' | i18n}}</p>
<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" </div>
type="text" <hr>
name="Email" <div class="d-flex">
[(ngModel)]="email" <button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
required <span>{{'submit' | i18n}}</span>
appAutofocus <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
inputmode="email" </button>
appInputVerbatim="false" <a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
/> {{'cancel' | i18n}}
</div> </a>
<hr /> </div>
<div class="d-flex"> </div>
<button </div>
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span>{{ "submit" | i18n }}</span>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
</div> </div>
</div>
</div> </div>
</div>
</form> </form>

View File

@@ -1,43 +1,33 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { Router } from "@angular/router"; import { Router } from '@angular/router';
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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { DeleteRecoverRequest } from "jslib-common/models/request/deleteRecoverRequest"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { DeleteRecoverRequest } from 'jslib-common/models/request/deleteRecoverRequest';
@Component({ @Component({
selector: "app-recover-delete", selector: 'app-recover-delete',
templateUrl: "recover-delete.component.html", templateUrl: 'recover-delete.component.html',
}) })
export class RecoverDeleteComponent { export class RecoverDeleteComponent {
email: string; email: string;
formPromise: Promise<any>; formPromise: Promise<any>;
constructor( constructor(private router: Router, private apiService: ApiService,
private router: Router, private toasterService: ToasterService, private i18nService: I18nService) {
private apiService: ApiService, }
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, async submit() {
private logService: LogService try {
) {} const request = new DeleteRecoverRequest();
request.email = this.email.trim().toLowerCase();
async submit() { this.formPromise = this.apiService.postAccountRecoverDelete(request);
try { await this.formPromise;
const request = new DeleteRecoverRequest(); this.toasterService.popAsync('success', null, this.i18nService.t('deleteRecoverEmailSent'));
request.email = this.email.trim().toLowerCase(); this.router.navigate(['/']);
this.formPromise = this.apiService.postAccountRecoverDelete(request); } catch { }
await this.formPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("deleteRecoverEmailSent")
);
this.router.navigate(["/"]);
} catch (e) {
this.logService.error(e);
} }
}
} }

View File

@@ -1,76 +1,40 @@
<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">{{ "recoverAccountTwoStep" | i18n }}</p> <p class="lead text-center mb-4">{{'recoverAccountTwoStep' | i18n}}</p>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<p> <p>{{'recoverAccountTwoStepDesc' | i18n}}
{{ "recoverAccountTwoStepDesc" | i18n }} <a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank"
<a rel="noopener">{{'learnMore' | i18n}}</a>
href="https://help.bitwarden.com/article/lost-two-step-device/" </p>
target="_blank" <div class="form-group">
rel="noopener" <label for="email">{{'emailAddress' | i18n}}</label>
>{{ "learnMore" | i18n }}</a <input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
> appAutofocus inputmode="email" appInputVerbatim="false">
</p> </div>
<div class="form-group"> <div class="form-group">
<label for="email">{{ "emailAddress" | i18n }}</label> <label for="masterPassword">{{'masterPass' | i18n}}</label>
<input <input id="masterPassword" type="password" name="MasterPassword" class="form-control"
id="email" [(ngModel)]="masterPassword" required appInputVerbatim>
class="form-control" </div>
type="text" <div class="form-group">
name="Email" <label for="recoveryCode">{{'recoveryCodeTitle' | i18n}}</label>
[(ngModel)]="email" <input id="recoveryCode" class="text-monospace form-control" type="text" name="RecoveryCode"
required [(ngModel)]="recoveryCode" required appInputVerbatim>
appAutofocus </div>
inputmode="email" <hr>
appInputVerbatim="false" <div class="d-flex">
/> <button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
</div> <span>{{'submit' | i18n}}</span>
<div class="form-group"> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<label for="masterPassword">{{ "masterPass" | i18n }}</label> </button>
<input <a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
id="masterPassword" {{'cancel' | i18n}}
type="password" </a>
name="MasterPassword" </div>
class="form-control" </div>
[(ngModel)]="masterPassword" </div>
required
appInputVerbatim
/>
</div>
<div class="form-group">
<label for="recoveryCode">{{ "recoveryCodeTitle" | i18n }}</label>
<input
id="recoveryCode"
class="text-monospace form-control"
type="text"
name="RecoveryCode"
[(ngModel)]="recoveryCode"
required
appInputVerbatim
/>
</div>
<hr />
<div class="d-flex">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span>{{ "submit" | i18n }}</span>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
</div> </div>
</div>
</div> </div>
</div>
</form> </form>

View File

@@ -1,52 +1,40 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { Router } from "@angular/router"; import { Router } from '@angular/router';
import { ApiService } from "jslib-common/abstractions/api.service"; import { ToasterService } from 'angular2-toaster';
import { AuthService } from "jslib-common/abstractions/auth.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 { TwoFactorRecoveryRequest } from "jslib-common/models/request/twoFactorRecoveryRequest"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { TwoFactorRecoveryRequest } from 'jslib-common/models/request/twoFactorRecoveryRequest';
@Component({ @Component({
selector: "app-recover-two-factor", selector: 'app-recover-two-factor',
templateUrl: "recover-two-factor.component.html", templateUrl: 'recover-two-factor.component.html',
}) })
export class RecoverTwoFactorComponent { export class RecoverTwoFactorComponent {
email: string; email: string;
masterPassword: string; masterPassword: string;
recoveryCode: string; recoveryCode: string;
formPromise: Promise<any>; formPromise: Promise<any>;
constructor( constructor(private router: Router, private apiService: ApiService,
private router: Router, private toasterService: ToasterService, private i18nService: I18nService,
private apiService: ApiService, private cryptoService: CryptoService, private authService: AuthService) { }
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private cryptoService: CryptoService,
private authService: AuthService,
private logService: LogService
) {}
async submit() { async submit() {
try { try {
const request = new TwoFactorRecoveryRequest(); const request = new TwoFactorRecoveryRequest();
request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase(); request.recoveryCode = this.recoveryCode.replace(/\s/g, '').toLowerCase();
request.email = this.email.trim().toLowerCase(); request.email = this.email.trim().toLowerCase();
const key = await this.authService.makePreloginKey(this.masterPassword, request.email); const key = await this.authService.makePreloginKey(this.masterPassword, request.email);
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key); request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
this.formPromise = this.apiService.postTwoFactorRecover(request); this.formPromise = this.apiService.postTwoFactorRecover(request);
await this.formPromise; await this.formPromise;
this.platformUtilsService.showToast( this.toasterService.popAsync('success', null, this.i18nService.t('twoStepRecoverDisabled'));
"success", this.router.navigate(['/']);
null, } catch { }
this.i18nService.t("twoStepRecoverDisabled")
);
this.router.navigate(["/"]);
} catch (e) {
this.logService.error(e);
} }
}
} }

View File

@@ -1,216 +1,141 @@
<div class="layout" [ngClass]="['layout', layout]"> <div class="layout" [ngClass]="['layout', layout]">
<header class="header" *ngIf="layout === 'enterprise2'"> <header class="header" *ngIf="layout === 'enterprise2'">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-7"> <div class="col-7">
<img <img alt="Bitwarden" class="logo mb-2" src="../../images/register-layout/logo-horizontal-white.png">
alt="Bitwarden"
class="logo mb-2"
src="../../images/register-layout/logo-horizontal-white.png"
/>
</div>
</div>
</div>
</header>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row">
<div class="col-7" *ngIf="layout">
<div class="mt-5">
<div *ngIf="layout === 'enterprise2'">
<h2>Companies globally trust Bitwarden for password management.</h2>
<p>Start your 7-day free trial!</p>
<p class="highlight">Quickly deploy your <b>organization</b></p>
<p>Use Bitwarden across all platforms</p>
<p>Collaborate and share securely</p>
<figure>
<figcaption>
<cite>
<img src="../../images/register-layout/wired-logo.png" alt="Wired" />
</cite>
</figcaption>
<blockquote>
"Bitwarden has become a popular choice among open-source software advocates. After
using it for a few months, I can see why." - February 2020
</blockquote>
</figure>
</div>
<div *ngIf="layout === 'enterprise3'">
<p>Enterprise 3 layout</p>
</div>
<div *ngIf="layout === 'enterprise4'">
<p>Enterprise 4 layout</p>
</div>
</div>
</div>
<div [ngClass]="{ 'col-5': layout, 'col-12': !layout }">
<div class="row justify-content-md-center mt-5">
<div [ngClass]="{ 'col-5': !layout, 'col-12': layout }">
<p class="lead text-center mb-4" *ngIf="!layout">{{ "createAccount" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<app-callout
title="{{ 'createOrganizationStep1' | i18n }}"
type="info"
icon="fa-thumb-tack"
*ngIf="showCreateOrgMessage"
>
{{ "createOrganizationCreatePersonalAccount" | i18n }}
</app-callout>
<div class="form-group">
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
[appAutofocus]="email === ''"
inputmode="email"
appInputVerbatim="false"
/>
<small class="form-text text-muted">{{ "emailAddressDesc" | i18n }}</small>
</div> </div>
<div class="form-group">
<label for="name">{{ "yourName" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="name"
[appAutofocus]="email !== ''"
/>
<small class="form-text text-muted">{{ "yourNameDesc" | i18n }}</small>
</div>
<div class="form-group">
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<div class="d-flex">
<div class="w-100">
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword"
(input)="updatePasswordStrength()"
required
appInputVerbatim
/>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{
'fa-eye': !showPassword,
'fa-eye-slash': showPassword
}"
></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{ "masterPassDesc" | i18n }}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<div class="d-flex">
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="text-monospace form-control"
[(ngModel)]="confirmMasterPassword"
required
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input
id="hint"
class="form-control"
type="text"
name="Hint"
[(ngModel)]="hint"
/>
<small class="form-text text-muted">{{ "masterPassHintDesc" | i18n }}</small>
</div>
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<div class="form-group" *ngIf="showTerms">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="acceptPolicies"
[(ngModel)]="acceptPolicies"
name="AcceptPolicies"
/>
<label class="form-check-label small text-muted" for="acceptPolicies">
{{ "acceptPolicies" | i18n }}<br />
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{
"termsOfService" | i18n
}}</a
>,
<a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{
"privacyPolicy" | i18n
}}</a>
</label>
</div>
</div>
<hr />
<div class="d-flex mb-2">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span>{{ "submit" | i18n }}</span>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
</div>
</div> </div>
</div>
</div> </div>
</div> </header>
</div> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
</form> <div class="row">
<div class="col-7" *ngIf="layout">
<div class="mt-5">
<div *ngIf="layout === 'enterprise2'">
<h2>Companies globally trust Bitwarden for password management.</h2>
<p>Start your 7-day free trial!</p>
<p class="highlight">Quickly deploy your <b>organization</b></p>
<p>Use Bitwarden across all platforms</p>
<p>Collaborate and share securely</p>
<figure>
<figcaption>
<cite>
<img src="../../images/register-layout/wired-logo.png" alt="Wired">
</cite>
</figcaption>
<blockquote>
"Bitwarden has become a popular choice among open-source software advocates. After using
it for a few months, I can see why." - February 2020
</blockquote>
</figure>
</div>
<div *ngIf="layout === 'enterprise3'">
<p>Enterprise 3 layout</p>
</div>
<div *ngIf="layout === 'enterprise4'">
<p>Enterprise 4 layout</p>
</div>
</div>
</div>
<div [ngClass]="{'col-5': layout, 'col-12': !layout}">
<div class="row justify-content-md-center mt-5">
<div [ngClass]="{'col-5': !layout, 'col-12': layout}">
<p class="lead text-center mb-4" *ngIf="!layout">{{'createAccount' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<app-callout title="{{'createOrganizationStep1' | i18n}}" type="info"
icon="fa-thumb-tack" *ngIf="showCreateOrgMessage">
{{'createOrganizationCreatePersonalAccount' | i18n}}
</app-callout>
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email"
required [appAutofocus]="email === ''" inputmode="email"
appInputVerbatim="false">
<small class="form-text text-muted">{{'emailAddressDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="name">{{'yourName' | i18n}}</label>
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name"
[appAutofocus]="email !== ''">
<small class="form-text text-muted">{{'yourNameDesc' | i18n}}</small>
</div>
<div class="form-group">
<app-callout type="info" [enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions">
</app-callout>
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<div class="w-100">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
appInputVerbatim>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div>
<button type="button" class="ml-1 btn btn-link"
appA11yTitle="{{'toggleVisibility' | i18n}}"
(click)="togglePassword(false)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPasswordRetype" class="text-monospace form-control"
[(ngModel)]="confirmMasterPassword" required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link"
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(true)">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{'masterPassHint' | i18n}}</label>
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
</div>
<div [hidden]="!showCaptcha()"><iframe id="hcaptcha_iframe" height="80"></iframe></div>
<div class="form-group" *ngIf="showTerms">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="acceptPolicies"
[(ngModel)]="acceptPolicies" name="AcceptPolicies">
<label class="form-check-label small text-muted" for="acceptPolicies">
{{'acceptPolicies' | i18n}}<br>
<a href="https://bitwarden.com/terms/" target="_blank"
rel="noopener">{{'termsOfService' | i18n}}</a>,
<a href="https://bitwarden.com/privacy/" target="_blank"
rel="noopener">{{'privacyPolicy' | i18n}}</a>
</label>
</div>
</div>
<hr>
<div class="d-flex mb-2">
<button type="submit" class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading">
<span>{{'submit' | i18n}}</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}"
aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div> </div>

View File

@@ -1,150 +1,105 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute, Router } from "@angular/router"; import {
ActivatedRoute,
Router,
} from '@angular/router';
import { first } from "rxjs/operators"; import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.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 { ApiService } from "jslib-common/abstractions/api.service"; import { RegisterComponent as BaseRegisterComponent } from 'jslib-angular/components/register.component';
import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoService } from "jslib-common/abstractions/crypto.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 { RegisterComponent as BaseRegisterComponent } from "jslib-angular/components/register.component"; import { MasterPasswordPolicyOptions } from 'jslib-common/models/domain/masterPasswordPolicyOptions';
import { Policy } from 'jslib-common/models/domain/policy';
import { MasterPasswordPolicyOptions } from "jslib-common/models/domain/masterPasswordPolicyOptions"; import { PolicyData } from 'jslib-common/models/data/policyData';
import { Policy } from "jslib-common/models/domain/policy"; import { ReferenceEventRequest } from 'jslib-common/models/request/referenceEventRequest';
import { PolicyData } from "jslib-common/models/data/policyData";
import { ReferenceEventRequest } from "jslib-common/models/request/referenceEventRequest";
@Component({ @Component({
selector: "app-register", selector: 'app-register',
templateUrl: "register.component.html", templateUrl: 'register.component.html',
}) })
export class RegisterComponent extends BaseRegisterComponent { export class RegisterComponent extends BaseRegisterComponent {
showCreateOrgMessage = false; showCreateOrgMessage = false;
layout = ""; layout = '';
enforcedPolicyOptions: MasterPasswordPolicyOptions; enforcedPolicyOptions: MasterPasswordPolicyOptions;
private policies: Policy[]; private policies: Policy[];
constructor( constructor(authService: AuthService, router: Router,
authService: AuthService, i18nService: I18nService, cryptoService: CryptoService,
router: Router, apiService: ApiService, private route: ActivatedRoute,
i18nService: I18nService, stateService: StateService, platformUtilsService: PlatformUtilsService,
cryptoService: CryptoService, passwordGenerationService: PasswordGenerationService, private policyService: PolicyService,
apiService: ApiService, environmentService: EnvironmentService) {
private route: ActivatedRoute, super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
stateService: StateService, passwordGenerationService, environmentService);
platformUtilsService: PlatformUtilsService, }
passwordGenerationService: PasswordGenerationService,
private policyService: PolicyService,
environmentService: EnvironmentService,
logService: LogService
) {
super(
authService,
router,
i18nService,
cryptoService,
apiService,
stateService,
platformUtilsService,
passwordGenerationService,
environmentService,
logService
);
}
async ngOnInit() { async ngOnInit() {
this.route.queryParams.pipe(first()).subscribe((qParams) => { const queryParamsSub = this.route.queryParams.subscribe(qParams => {
this.referenceData = new ReferenceEventRequest(); this.referenceData = new ReferenceEventRequest();
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.stateService.setLoginRedirect({ route: "/settings/premium" }); this.stateService.save('loginRedirect', { route: '/settings/premium' });
} else if (qParams.org != null) { } else if (qParams.org != null) {
this.showCreateOrgMessage = true; this.showCreateOrgMessage = true;
this.referenceData.flow = qParams.org; this.referenceData.flow = qParams.org;
this.stateService.setLoginRedirect({ this.stateService.save('loginRedirect',
route: "/settings/create-organization", { route: '/settings/create-organization', qParams: { plan: qParams.org } });
qParams: { plan: qParams.org }, }
if (qParams.layout != null) {
this.layout = this.referenceData.layout = qParams.layout;
}
if (qParams.reference != null) {
this.referenceData.id = qParams.reference;
} else {
this.referenceData.id = ('; ' + document.cookie).split('; reference=').pop().split(';').shift();
}
if (this.referenceData.id === '') {
this.referenceData.id = null;
}
if (queryParamsSub != null) {
queryParamsSub.unsubscribe();
}
}); });
} const invite = await this.stateService.get<any>('orgInvitation');
if (qParams.layout != null) { if (invite != null) {
this.layout = this.referenceData.layout = qParams.layout; try {
} const policies = await this.apiService.getPoliciesByToken(invite.organizationId, invite.token,
if (qParams.reference != null) { invite.email, invite.organizationUserId);
this.referenceData.id = qParams.reference; if (policies.data != null) {
} else { const policiesData = policies.data.map(p => new PolicyData(p));
this.referenceData.id = ("; " + document.cookie) this.policies = policiesData.map(p => new Policy(p));
.split("; reference=") }
.pop() } catch { }
.split(";")
.shift();
}
// Are they coming from an email for sponsoring a families organization
if (qParams.sponsorshipToken != null) {
// After logging in redirect them to setup the families sponsorship
this.stateService.setLoginRedirect({
route: "/setup/families-for-enterprise",
qParams: { token: qParams.sponsorshipToken },
});
}
if (this.referenceData.id === "") {
this.referenceData.id = null;
}
});
const invite = await this.stateService.getOrganizationInvitation();
if (invite != null) {
try {
const policies = await this.apiService.getPoliciesByToken(
invite.organizationId,
invite.token,
invite.email,
invite.organizationUserId
);
if (policies.data != null) {
const policiesData = policies.data.map((p) => new PolicyData(p));
this.policies = policiesData.map((p) => new Policy(p));
} }
} catch (e) {
this.logService.error(e); if (this.policies != null) {
} this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(this.policies);
}
await super.ngOnInit();
} }
if (this.policies != null) { async submit() {
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions( if (this.enforcedPolicyOptions != null &&
this.policies !this.policyService.evaluateMasterPassword(this.masterPasswordScore, this.masterPassword,
); this.enforcedPolicyOptions)) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('masterPasswordPolicyRequirementsNotMet'));
return;
}
await super.submit();
} }
await super.ngOnInit();
}
async submit() {
if (
this.enforcedPolicyOptions != null &&
!this.policyService.evaluateMasterPassword(
this.masterPasswordScore,
this.masterPassword,
this.enforcedPolicyOptions
)
) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("masterPasswordPolicyRequirementsNotMet")
);
return;
}
await super.submit();
}
} }

View File

@@ -1,55 +0,0 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div>
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center">
<i
class="fa fa-spinner fa-spin fa-2x text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</div>
</div>
<div class="container" *ngIf="!loading">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "removeMasterPassword" | i18n }}</p>
<hr />
<div class="card d-block">
<div class="card-body">
<p>{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}</p>
<button
type="button"
class="btn btn-primary btn-block"
(click)="convert()"
[disabled]="actionPromise"
>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="continuing"
></i>
{{ "removeMasterPassword" | i18n }}
</button>
<button
type="button"
class="btn btn-outline-secondary btn-block"
(click)="leave()"
[disabled]="actionPromise"
>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="leaving"
></i>
{{ "leaveOrganization" | i18n }}
</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,9 +0,0 @@
import { Component } from "@angular/core";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "jslib-angular/components/remove-password.component";
@Component({
selector: "app-remove-password",
templateUrl: "remove-password.component.html",
})
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}

View File

@@ -1,117 +1,73 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<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">{{ "setMasterPassword" | i18n }}</p> <p class="lead text-center mb-4">{{'setMasterPassword' | i18n}}</p>
<div class="card d-block"> <div class="card d-block">
<div class="card-body text-center" *ngIf="syncLoading"> <div class="card-body text-center" *ngIf="syncLoading">
<i class="fa fa-spinner fa-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>
<div class="card-body" *ngIf="!syncLoading"> <div class="card-body" *ngIf="!syncLoading">
<app-callout type="info">{{ "ssoCompleteRegistration" | i18n }}</app-callout> <app-callout type="info">{{'ssoCompleteRegistration' | i18n}}</app-callout>
<app-callout <app-callout type="warning" title="{{'resetPasswordPolicyAutoEnroll' | i18n}}"
type="warning" *ngIf="resetPasswordAutoEnroll">
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}" {{'resetPasswordAutoEnrollInviteWarning' | i18n}}
*ngIf="resetPasswordAutoEnroll" </app-callout>
> <div class="form-group">
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }} <app-callout type="info" [enforcedPolicyOptions]="enforcedPolicyOptions"
</app-callout> *ngIf="enforcedPolicyOptions">
<div class="form-group"> </app-callout>
<app-callout <label for="masterPassword">{{'masterPass' | i18n}}</label>
type="info" <div class="d-flex">
[enforcedPolicyOptions]="enforcedPolicyOptions" <div class="w-100">
*ngIf="enforcedPolicyOptions" <input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
> name="MasterPasswordHash" class="text-monospace form-control mb-1"
</app-callout> [(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required
<label for="masterPassword">{{ "masterPass" | i18n }}</label> appInputVerbatim>
<div class="d-flex"> <app-password-strength [score]="masterPasswordScore" [showText]="true">
<div class="w-100"> </app-password-strength>
<input </div>
id="masterPassword" <div>
type="{{ showPassword ? 'text' : 'password' }}" <button type="button" class="ml-1 btn btn-link"
name="MasterPasswordHash" appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword(false)">
class="text-monospace form-control mb-1" <i class="fa fa-lg" aria-hidden="true"
[(ngModel)]="masterPassword" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
(input)="updatePasswordStrength()" </button>
required <div class="progress-bar invisible"></div>
appInputVerbatim </div>
/> </div>
<app-password-strength [score]="masterPasswordScore" [showText]="true"> <small class="form-text text-muted">{{'masterPassDesc' | i18n}}</small>
</app-password-strength> </div>
</div> <div class="form-group">
<div> <label for="masterPasswordRetype">{{'reTypeMasterPass' | i18n}}</label>
<button <div class="d-flex">
type="button" <input id="masterPasswordRetype" type="{{showPassword ? 'text' : 'password'}}"
class="ml-1 btn btn-link" name="MasterPasswordRetype" class="text-monospace form-control"
appA11yTitle="{{ 'toggleVisibility' | i18n }}" [(ngModel)]="masterPasswordRetype" required appInputVerbatim>
(click)="togglePassword(false)" <button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
> (click)="togglePassword(true)">
<i <i class="fa fa-lg" aria-hidden="true"
class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
aria-hidden="true" </button>
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }" </div>
></i> </div>
</button> <div class="form-group">
<div class="progress-bar invisible"></div> <label for="hint">{{'masterPassHint' | i18n}}</label>
</div> <input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint">
<small class="form-text text-muted">{{'masterPassHintDesc' | i18n}}</small>
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span>{{'submit' | i18n}}</span>
</button>
<button type="button" class="btn btn-outline-secondary btn-block ml-2 mt-0" (click)="logOut()">
{{'logOut' | i18n}}
</button>
</div>
</div>
</div> </div>
<small class="form-text text-muted">{{ "masterPassDesc" | i18n }}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<div class="d-flex">
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="text-monospace form-control"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" class="form-control" type="text" name="Hint" [(ngModel)]="hint" />
<small class="form-text text-muted">{{ "masterPassHintDesc" | i18n }}</small>
</div>
<hr />
<div class="d-flex">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "submit" | i18n }}</span>
</button>
<button
type="button"
class="btn btn-outline-secondary btn-block ml-2 mt-0"
(click)="logOut()"
>
{{ "logOut" | i18n }}
</button>
</div>
</div> </div>
</div>
</div> </div>
</div>
</form> </form>

View File

@@ -1,48 +1,34 @@
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 { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from "jslib-common/abstractions/crypto.service"; import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { MessagingService } from "jslib-common/abstractions/messaging.service"; import { MessagingService } from 'jslib-common/abstractions/messaging.service';
import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service"; import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { PolicyService } from "jslib-common/abstractions/policy.service"; import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from "jslib-common/abstractions/state.service"; import { SyncService } from 'jslib-common/abstractions/sync.service';
import { SyncService } from "jslib-common/abstractions/sync.service"; import { UserService } from 'jslib-common/abstractions/user.service';
import { SetPasswordComponent as BaseSetPasswordComponent } from "jslib-angular/components/set-password.component"; import {
SetPasswordComponent as BaseSetPasswordComponent,
} from 'jslib-angular/components/set-password.component';
@Component({ @Component({
selector: "app-set-password", selector: 'app-set-password',
templateUrl: "set-password.component.html", templateUrl: 'set-password.component.html',
}) })
export class SetPasswordComponent extends BaseSetPasswordComponent { export class SetPasswordComponent extends BaseSetPasswordComponent {
constructor( constructor(apiService: ApiService, i18nService: I18nService,
apiService: ApiService, cryptoService: CryptoService, messagingService: MessagingService,
i18nService: I18nService, userService: UserService, passwordGenerationService: PasswordGenerationService,
cryptoService: CryptoService, platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router,
messagingService: MessagingService, syncService: SyncService, route: ActivatedRoute) {
passwordGenerationService: PasswordGenerationService, super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
platformUtilsService: PlatformUtilsService, platformUtilsService, policyService, router, apiService, syncService, route);
policyService: PolicyService, }
router: Router,
syncService: SyncService,
route: ActivatedRoute,
stateService: StateService
) {
super(
i18nService,
cryptoService,
messagingService,
passwordGenerationService,
platformUtilsService,
policyService,
router,
apiService,
syncService,
route,
stateService
);
}
} }

View File

@@ -1,52 +1,33 @@
<form <form #form (ngSubmit)="submit()" class="container" [appApiAction]="initiateSsoFormPromise" ngNativeValidate>
#form <div class="row justify-content-md-center mt-5">
(ngSubmit)="submit()" <div class="col-5">
class="container" <img class="logo mb-2 logo-themed" alt="Bitwarden">
[appApiAction]="initiateSsoFormPromise" <div class="card d-block mt-4">
ngNativeValidate <div class="card-body" *ngIf="loggingIn">
> <i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<div class="row justify-content-md-center mt-5"> {{'loading' | i18n}}
<div class="col-5"> </div>
<img class="logo mb-2 logo-themed" alt="Bitwarden" /> <div class="card-body" *ngIf="!loggingIn">
<div class="card d-block mt-4"> <p>{{'ssoLogInWithOrgIdentifier' | i18n}}</p>
<div class="card-body" *ngIf="loggingIn"> <div class="form-group">
<i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <label for="identifier">{{'organizationIdentifier' | i18n}}</label>
{{ "loading" | i18n }} <input id="identifier" class="form-control" type="text" name="Identifier"
[(ngModel)]="identifier" required appAutofocus>
</div>
<hr>
<div class="d-flex">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading">
<span>
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'logIn' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}
</a>
</div>
</div>
</div>
</div> </div>
<div class="card-body" *ngIf="!loggingIn">
<p>{{ "ssoLogInWithOrgIdentifier" | i18n }}</p>
<div class="form-group">
<label for="identifier">{{ "organizationIdentifier" | i18n }}</label>
<input
id="identifier"
class="form-control"
type="text"
name="Identifier"
[(ngModel)]="identifier"
required
appAutofocus
/>
</div>
<hr />
<div class="d-flex">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span> <i class="fa fa-sign-in" aria-hidden="true"></i> {{ "logIn" | i18n }} </span>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
</div>
</div>
</div> </div>
</div>
</form> </form>

View File

@@ -1,74 +1,62 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { ActivatedRoute, Router } from "@angular/router"; import {
ActivatedRoute,
Router,
} from '@angular/router';
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 { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { StorageService } from 'jslib-common/abstractions/storage.service';
import { ApiService } from "jslib-common/abstractions/api.service"; import { SsoComponent as BaseSsoComponent } from 'jslib-angular/components/sso.component';
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 { StateService } from "jslib-common/abstractions/state.service";
import { SsoComponent as BaseSsoComponent } from "jslib-angular/components/sso.component"; const IdentifierStorageKey = 'ssoOrgIdentifier';
@Component({ @Component({
selector: "app-sso", selector: 'app-sso',
templateUrl: "sso.component.html", templateUrl: 'sso.component.html',
}) })
export class SsoComponent extends BaseSsoComponent { export class SsoComponent extends BaseSsoComponent {
constructor( constructor(authService: AuthService, router: Router,
authService: AuthService, i18nService: I18nService, route: ActivatedRoute,
router: Router, storageService: StorageService, stateService: StateService,
i18nService: I18nService, platformUtilsService: PlatformUtilsService, apiService: ApiService,
route: ActivatedRoute, cryptoFunctionService: CryptoFunctionService, environmentService: EnvironmentService,
stateService: StateService, passwordGenerationService: PasswordGenerationService) {
platformUtilsService: PlatformUtilsService, super(authService, router, i18nService, route, storageService, stateService, platformUtilsService,
apiService: ApiService, apiService, cryptoFunctionService, environmentService, passwordGenerationService);
cryptoFunctionService: CryptoFunctionService, this.redirectUri = window.location.origin + '/sso-connector.html';
environmentService: EnvironmentService, this.clientId = 'web';
passwordGenerationService: PasswordGenerationService, }
logService: LogService
) { async ngOnInit() {
super( super.ngOnInit();
authService, const queryParamsSub = this.route.queryParams.subscribe(async qParams => {
router, if (qParams.identifier != null) {
i18nService, this.identifier = qParams.identifier;
route, } else {
stateService, const storedIdentifier = await this.storageService.get<string>(IdentifierStorageKey);
platformUtilsService, if (storedIdentifier != null) {
apiService, this.identifier = storedIdentifier;
cryptoFunctionService, }
environmentService, }
passwordGenerationService, if (queryParamsSub != null) {
logService queryParamsSub.unsubscribe();
); }
this.redirectUri = window.location.origin + "/sso-connector.html"; });
this.clientId = "web"; }
}
async submit() {
async ngOnInit() { await this.storageService.save(IdentifierStorageKey, this.identifier);
super.ngOnInit(); if (this.clientId === 'browser') {
this.route.queryParams.pipe(first()).subscribe(async (qParams) => { document.cookie = `ssoHandOffMessage=${this.i18nService.t('ssoHandOff')};SameSite=strict`;
if (qParams.identifier != null) { }
this.identifier = qParams.identifier; super.submit();
} else {
const storedIdentifier = await this.stateService.getSsoOrgIdentifier();
if (storedIdentifier != null) {
this.identifier = storedIdentifier;
}
}
});
}
async submit() {
await this.stateService.setSsoOrganizationIdentifier(this.identifier);
if (this.clientId === "browser") {
document.cookie = `ssoHandOffMessage=${this.i18nService.t("ssoHandOff")};SameSite=strict`;
} }
super.submit();
}
} }

View File

@@ -1,68 +1,29 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="twoStepOptionsTitle"> <div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="twoStepOptionsTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document"> <div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title" id="twoStepOptionsTitle">{{ "twoStepOptions" | i18n }}</h2> <h2 class="modal-title" id="twoStepOptionsTitle">{{'twoStepOptions' | i18n}}</h2>
<button <button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
type="button" <span aria-hidden="true">&times;</span>
class="close"
data-dismiss="modal"
appA11yTitle="{{ 'close' | i18n }}"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="list-group list-group-flush-2fa">
<div *ngFor="let p of providers" class="list-group-item list-group-item-action">
<div class="two-factor-content">
<div class="logo-col">
<img [class]="'mfaType' + p.type" [alt]="p.name + ' logo'" />
</div>
<div class="text-col">
<h3>{{ p.name }}</h3>
{{ p.description }}
</div>
<div class="btn-col">
<button
[attr.aria-describedby]="p.name"
type="button"
class="btn btn-outline-secondary btn-sm"
(click)="choose(p)"
>
{{ "select" | i18n }}
</button> </button>
</div>
</div> </div>
</div> <div class="modal-body">
<div class="list-group-item list-group-item-action" (click)="recover()"> <div class="list-group list-group-flush">
<div class="two-factor-content"> <a href="#" appStopClick *ngFor="let p of providers" (click)="choose(p)"
<div class="logo-col"> class="list-group-item list-group-item-action">
<img class="recovery-code-img" alt="rc logo" /> <img [src]="'images/two-factor/' + p.type + '.png'" alt="" class="pull-right">
</div> <h3>{{p.name}}</h3>
<div class="text-col"> {{p.description}}
<h3>{{ "recoveryCodeTitle" | i18n }}</h3> </a>
{{ "recoveryCodeDesc" | i18n }} <a href="#" appStopClick class="list-group-item list-group-item-action" (click)="recover()">
</div> <h3>{{'recoveryCodeTitle' | i18n}}</h3>
<div class="btn-col"> {{'recoveryCodeDesc' | i18n}}
<button </a>
[attr.aria-descibedby]="'recoveryCodeTitle' | i18n" </div>
type="button" </div>
class="btn btn-outline-secondary btn-sm" <div class="modal-footer">
(click)="recover()" <button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
>
{{ "select" | i18n }}
</button>
</div>
</div> </div>
</div>
</div> </div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
{{ "close" | i18n }}
</button>
</div>
</div> </div>
</div>
</div> </div>

View File

@@ -1,23 +1,21 @@
import { Component } from "@angular/core"; import { Component } from '@angular/core';
import { Router } from "@angular/router"; import { Router } from '@angular/router';
import { AuthService } from "jslib-common/abstractions/auth.service"; import { AuthService } from 'jslib-common/abstractions/auth.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 { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "jslib-angular/components/two-factor-options.component"; import {
TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent,
} from 'jslib-angular/components/two-factor-options.component';
@Component({ @Component({
selector: "app-two-factor-options", selector: 'app-two-factor-options',
templateUrl: "two-factor-options.component.html", templateUrl: 'two-factor-options.component.html',
}) })
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
constructor( constructor(authService: AuthService, router: Router,
authService: AuthService, i18nService: I18nService, platformUtilsService: PlatformUtilsService) {
router: Router, super(authService, router, i18nService, platformUtilsService, window);
i18nService: I18nService, }
platformUtilsService: PlatformUtilsService
) {
super(authService, router, i18nService, platformUtilsService, window);
}
} }

View File

@@ -1,152 +1,80 @@
<form <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate autocomplete="off">
#form <div class="row justify-content-md-center mt-5">
(ngSubmit)="submit()" <div class="col-5"
[appApiAction]="formPromise" [ngClass]="{'col-9': selectedProviderType === providerType.Duo || selectedProviderType === providerType.OrganizationDuo}">
class="container" <p class="lead text-center mb-4">{{title}}</p>
ngNativeValidate <div class="card d-block">
autocomplete="off" <div class="card-body">
> <ng-container
<div class="row justify-content-md-center mt-5"> *ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
<div <p *ngIf="selectedProviderType === providerType.Authenticator">
class="col-5" {{'enterVerificationCodeApp' | i18n}}</p>
[ngClass]="{ <p *ngIf="selectedProviderType === providerType.Email">
'col-9': {{'enterVerificationCodeEmail' | i18n : twoFactorEmail}}
selectedProviderType === providerType.Duo || </p>
selectedProviderType === providerType.OrganizationDuo <div class="form-group">
}" <label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
> <input id="code" type="text" name="Code" class="form-control" [(ngModel)]="token" required
<p class="lead text-center mb-4">{{ title }}</p> appAutofocus inputmode="tel" appInputVerbatim>
<div class="card d-block"> <small class="form-text" *ngIf="selectedProviderType === providerType.Email">
<div class="card-body"> <a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise"
<ng-container *ngIf="selectedProviderType === providerType.Email">
*ngIf=" {{'sendVerificationCodeEmailAgain' | i18n}}
selectedProviderType === providerType.Email || </a>
selectedProviderType === providerType.Authenticator </small>
" </div>
> </ng-container>
<p *ngIf="selectedProviderType === providerType.Authenticator"> <ng-container *ngIf="selectedProviderType === providerType.Yubikey">
{{ "enterVerificationCodeApp" | i18n }} <p class="text-center">{{'insertYubiKey' | i18n}}</p>
</p> <img src="../../images/yubikey.jpg" class="rounded img-fluid mb-3" alt="">
<p *ngIf="selectedProviderType === providerType.Email"> <div class="form-group">
{{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }} <label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
</p> <input id="code" type="password" name="Code" class="form-control" [(ngModel)]="token"
<div class="form-group"> required appAutofocus appInputVerbatim autocomplete="new-password">
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label> </div>
<input </ng-container>
id="code" <ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
type="text" <div id="web-authn-frame" class="mb-3">
name="Code" <iframe id="webauthn_iframe" [allow]="webAuthnAllow"></iframe>
class="form-control" </div>
[(ngModel)]="token" </ng-container>
required <ng-container *ngIf="selectedProviderType === providerType.Duo ||
appAutofocus selectedProviderType === providerType.OrganizationDuo">
inputmode="tel" <div id="duo-frame" class="mb-3">
appInputVerbatim <iframe id="duo_iframe"></iframe>
/> </div>
<small class="form-text" *ngIf="selectedProviderType === providerType.Email"> </ng-container>
<a <i class="fa fa-spinner text-muted fa-spin pull-right" title="{{'loading' | i18n}}"
href="#" *ngIf="form.loading && selectedProviderType === providerType.WebAuthn" aria-hidden="true"></i>
appStopClick <div class="form-check" *ngIf="selectedProviderType != null">
(click)="sendEmail(true)" <input id="remember" type="checkbox" name="Remember" class="form-check-input"
[appApiAction]="emailPromise" [(ngModel)]="remember">
*ngIf="selectedProviderType === providerType.Email" <label for="remember" class="form-check-label">{{'rememberMe' | i18n}}</label>
> </div>
{{ "sendVerificationCodeEmailAgain" | i18n }} <ng-container *ngIf="selectedProviderType == null">
</a> <p>{{'noTwoStepProviders' | i18n}}</p>
</small> <p>{{'noTwoStepProviders2' | i18n}}</p>
</ng-container>
<hr>
<div class="d-flex mb-3">
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading"
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.WebAuthn">
<span>
<i class="fa fa-sign-in" aria-hidden="true"></i> {{'continue' | i18n}}
</span>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{'cancel' | i18n}}
</a>
</div>
<div class="text-center">
<a href="#" appStopClick (click)="anotherMethod()">{{'useAnotherTwoStepMethod' | i18n}}</a>
</div>
</div>
</div> </div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
<p class="text-center">{{ "insertYubiKey" | i18n }}</p>
<picture>
<source srcset="../../images/yubikey.avif" type="image/avif" />
<source srcset="../../images/yubikey.webp" type="image/webp" />
<img src="../../images/yubikey.jpg" class="rounded img-fluid mb-3" alt="" />
</picture>
<div class="form-group">
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label>
<input
id="code"
type="password"
name="Code"
class="form-control"
[(ngModel)]="token"
required
appAutofocus
appInputVerbatim
autocomplete="new-password"
/>
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
<div id="web-authn-frame" class="mb-3">
<iframe id="webauthn_iframe" [allow]="webAuthnAllow"></iframe>
</div>
</ng-container>
<ng-container
*ngIf="
selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo
"
>
<div id="duo-frame" class="mb-3">
<iframe id="duo_iframe"></iframe>
</div>
</ng-container>
<i
class="fa fa-spinner text-muted fa-spin pull-right"
title="{{ 'loading' | i18n }}"
*ngIf="form.loading && selectedProviderType === providerType.WebAuthn"
aria-hidden="true"
></i>
<div class="form-check" *ngIf="selectedProviderType != null">
<input
id="remember"
type="checkbox"
name="Remember"
class="form-check-input"
[(ngModel)]="remember"
/>
<label for="remember" class="form-check-label">{{ "rememberMe" | i18n }}</label>
</div>
<ng-container *ngIf="selectedProviderType == null">
<p>{{ "noTwoStepProviders" | i18n }}</p>
<p>{{ "noTwoStepProviders2" | i18n }}</p>
</ng-container>
<hr />
<div class="d-flex mb-3">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
*ngIf="
selectedProviderType != null &&
selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo &&
selectedProviderType !== providerType.WebAuthn
"
>
<span>
<i class="fa fa-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }}
</span>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
<div class="text-center">
<a href="#" appStopClick (click)="anotherMethod()">{{
"useAnotherTwoStepMethod" | i18n
}}</a>
</div>
</div> </div>
</div>
</div> </div>
</div>
</form> </form>
<ng-template #twoFactorOptions></ng-template> <ng-template #twoFactorOptions></ng-template>

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