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

Compare commits

...

71 Commits

Author SHA1 Message Date
Joseph Flinn
f244f4ffde More work on the decoupling. It is not currently in a working state 2022-01-28 22:41:45 +00:00
Joseph Flinn
9d393224da switching to an nginx image instead of a bitwarden/server one 2022-01-28 12:01:47 -08:00
Addison Beck
ce1ae208d1 [bug] Update theme.js to refelect new storage structure (#1416)
* [bug] Update theme.js to refelect new storage structure

* [bug] Remove unecassary defaults
2022-01-28 11:30:45 -05:00
Addison Beck
6996b06fa2 [chore] Update jslib (#1415) 2022-01-28 09:07:33 -05:00
github-actions[bot]
dc503d3461 Autosync the updated translations (#1414)
Co-authored-by: github-actions <>
2022-01-28 11:06:25 +01:00
Daniel James Smith
d95db8fb74 BEEEP: Add importer for Keeper in json format (#1392)
* Updated instructions to export a csv file from Keeper

* Add instructions to export a json file from Keeper

* Bump jslib to include Keeper json importer

* Revert change to README.md

* Pull in jslib
2022-01-27 21:51:56 +01:00
Justin Baur
1a219daa12 Remove F4E vault card (#1413)
* Remove F4E card from vault page

* Remove unneeded property
2022-01-27 15:47:56 -05:00
Vincent Salucci
2ae98887b7 [Icons] Update Font Sheet (#1343)
* [Icons] Update to new font sheet

* Rebased - updated all icon remaining icon references

* Temporarily Updating gitmodules branch

* Fixed class reference

* Revert temporary gitmodule branch

* Icon updates/changes

* Pull jslib m-icon-updates latest

* Prettier

* Update jslib to master

* Reset jslib to master

* Removed obsolete variable reference, replaced bolt references

* Removed all instances of base class - maps create automatically

* Updated toast icon references

* Imported styles to reference variable/map

* Reverted to using base class

* Update jslib

* Rename eye-2 to eye and eye-slash-2 to eye-slash

* Bump jslib

* Remove duplicate scss

* Remove old fa

* Update fallback image

* Bump jslib

* Rename eye-2 to eye, and eye-slash-2 to eye-slash

* Fix 404

* Fix integrity of bootstrap.min.css

* Fix callout missing bwi

* Add bwi to change-kdf

* Remove bwi from callout again

* Bump jslib

Co-authored-by: Hinton <oscar@oscarhinton.com>
2022-01-27 11:25:58 -06:00
Joseph Flinn
f0c47252e4 Updating the base Docker image for testing purposes (#1411) 2022-01-27 07:25:13 -08:00
Danny Murphy
2ffe3bd6ad Cleanup of the SCSS Variables (#1255)
* Clean up variable names and comments

* Fix Option Colour - issue #1338

* Update old scss variable name

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
2022-01-26 14:54:15 +10:00
Oscar Hinton
f387a4d469 Remove old u2f connector (#1406) 2022-01-21 15:00:53 +01:00
github-actions[bot]
a0f1b4dd0d Autosync the updated translations (#1404)
Co-authored-by: github-actions <>
2022-01-21 11:53:09 +01:00
Thomas Rittson
84a65edc08 Move keyConnectorService call to syncService (#1405) 2022-01-21 19:33:09 +10:00
Addison Beck
caad11c571 [chore] Update jslib (#1403) 2022-01-20 09:38:34 -05:00
Daniel James Smith
b73449159d Update copy on fingerprint phrase prompt (#1399) 2022-01-20 10:07:47 +01:00
Oscar Hinton
bf48434d0f Rename package to @bitwarden/web-vault (#1396) 2022-01-17 17:23:14 +01:00
Oscar Hinton
b6d2d5bf71 [BEEEP] Use shared tsconfig (#1393) 2022-01-17 15:53:19 +01:00
Oscar Hinton
dfd62c7c3a Fix webpack assets using wrong name (#1391) 2022-01-14 13:35:44 +01:00
github-actions[bot]
41d3bd8cf2 Autosync the updated translations (#1389)
Co-authored-by: github-actions <>
2022-01-14 11:21:40 +01:00
Vince Grassia
3292d119fe Update Version Bump action (#1388) 2022-01-12 16:06:50 -05:00
Christian Oliff
b8de92435b Add Inputmode for tel and email (identities) (#1384) 2022-01-12 10:07:56 +01:00
Oscar Hinton
fd1d512a0f Run prettier on #1232 (#1383) 2022-01-10 14:50:54 +01:00
Daniel James Smith
14b8903d9a Fix items not opening when they had a password reprompt set (#1381) 2022-01-10 14:12:35 +01:00
Simon Legner
45284eefb3 Compress images u2fkey/yubikey using avif/webp (#1232)
Co-authored-by: Hinton <oscar@oscarhinton.com>
2022-01-10 12:37:21 +01:00
Daniel James Smith
49f6cfab7f Fixed linting issues (ran prettier) (#1379) 2022-01-07 14:28:25 +01:00
github-actions[bot]
2d271460e3 Autosync the updated translations (#1378)
Co-authored-by: github-actions <>
2022-01-07 13:51:08 +01:00
github-actions[bot]
241004f13b Bumped version to 2.25.1 (#1376)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-01-06 14:15:35 -08:00
Oscar Hinton
2f5d0201fe [BEEEP] Add script for optimizing images (#1374) 2022-01-06 21:20:35 +01:00
Daniel James Smith
7ffb5db310 Add --ignore-unknown to prettier on lint-staged (#1375) 2022-01-06 16:47:55 +01:00
Daniel James Smith
6603521d88 Add all filetypes to prettier and ignore via .prettierignore (#1373) 2022-01-06 15:13:29 +01:00
Addison Beck
d066e0586a [bug] Add defaults for vault timeout (#1365)
* [bug] Add state defaults for vault timeout

* [chore] Update jslib
2022-01-04 11:15:58 -05:00
Daniel James Smith
d0e661b84b Update year in copyright (#1370) 2022-01-03 17:14:50 +01:00
github-actions[bot]
6fa77cef88 Autosync the updated translations (#1368)
Co-authored-by: github-actions <>
2022-01-01 17:51:25 +01:00
Robyn MacCallum
6f408b871f Rename fb to formBuilder (#1369) 2021-12-31 10:06:07 -05:00
Addison Beck
8a9b992757 [bug(Account Switching)] Allow for never lock for dev setups (#1345)
* [bug(Account Switching)] Allow for never lock for dev setups

* [chore] Remove unecassary import

* [style] Ran prettier

* [chore] Update jslib
2021-12-28 10:47:41 -05:00
github-actions[bot]
55ecc4b804 Autosync the updated translations (#1359)
Co-authored-by: github-actions <>
2021-12-24 01:09:22 +01:00
Joseph Flinn
a71ce448f4 Change QA deploy SP & Re-enable feature branch deploy (#1358) 2021-12-23 09:14:10 -05:00
Jake Fink
bc82ae961e update jslib (#1356) 2021-12-21 13:09:09 -05:00
Linus Aarnio
ebcfdcd8a4 Add credit card logos to allow displaying icons based on brand (#1280)
Co-authored-by: Hinton <oscar@oscarhinton.com>
2021-12-21 12:08:08 +01:00
Vince Grassia
8991dcbf32 Set image tag to be 'dev' for now (#1353) 2021-12-20 13:16:53 -05:00
Micaiah Martin
cc9b9c91d7 Patch CI to allow for redeployments. Add Github actions to prettier .ignore as well. (#1352)
* Added inputs for reruns

* Added github workflows to .prettierignore

* Moved the Redeploy logic into the setup job
2021-12-20 10:15:43 -07:00
Daniel James Smith
3880d60101 Fix missing translation for unlinking SSO dialog (#1350) 2021-12-17 21:11:02 +01:00
Jake Fink
f5fdb34f7d Bug/sso properties globalstate (#1342)
* move sso properties in jslib to globalstate

* update jslib

* update jslib with prettier changes
2021-12-17 11:24:58 -05:00
Oscar Hinton
5b8f2034c3 Add .git-blame-ignore-revs (#1349) 2021-12-17 16:07:28 +01:00
Oscar Hinton
56477eb39c Apply Prettier (#1347) 2021-12-17 15:57:11 +01:00
Oscar Hinton
2b0a9d995e Add Prettier configuration (#1346) 2021-12-17 15:44:44 +01:00
github-actions[bot]
595722dfa1 Autosync the updated translations (#1348)
Co-authored-by: github-actions <>
2021-12-17 01:17:36 +01:00
Vince Grassia
6a1e683a93 Update workflows (#1344) 2021-12-16 11:46:26 -05:00
Addison Beck
97ca771a00 [bug] Correct bad BroadcasterService import (#1341)
* [bug] Correct bad BroadcasterService import

* [chore] update jslib
2021-12-14 22:07:56 -05:00
Jake Fink
214f82e142 send configType and redirectBehavior as int instead of string for request (#1339) 2021-12-14 13:25:48 -05:00
Addison Beck
17ae5ee57c [Account Switching] [Refactor] Implement new account centric services (#1220)
* [chore] updated services.module to use account services

* [refactor] sorted services provided by services.module

* [chore] removed references to deleted jslib services

* [chore] used activeAccount over storageService for account level storage items

* [chore] resolved linter warnings

* Refactor activeAccountService to stateService

* [bug] Remove uneeded calls to state service on logout

This was causing console erros on logout. Clearing of data is handled fully in dedicated services, clearing them in state afterwards is essentially a redundant call.

* [bug] Add back null locked callback to VaultTimeoutService

* Move call to get showUpdateKey

* [bug] Ensure HtmlStorageService does not override StateService options and locations

* [bug] Adjust theme logic to pull from the new storage locations

* [bug] Correct theme not sticking on refresh

* [bug] Add enableFullWidth to the account model

* [bug] fix theme option empty when light is selected

* [bug] init state on application start

* [bug] Reinit state when coming back from a lock

* [style] Fix lint complaints

* [bug] Clean state on logout

* [chore] Resolved merge issues

* [bug] Correct default for enableGravitars

* Bump angular to 12.

* Remove angular.json

* Bump rxjs

* Fix build errors, remove file-loader with asset/resource

* Use contenthash

* Bump jslib

* Bump ngx-toastr

* [chore] resolve issues from merge

* [chore] resolve issues from merge

* [bug] Add missing bracket

* Use newer import syntax

* [bug] Correct service orge

* [style] Fix lint complaints

* [chore] update jslib

* [review] Address code review

* [review] Address code review

* [review] Rename providerService to webProviderService

Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>
2021-12-14 11:10:26 -05:00
Daniel James Smith
71075cf878 Bump node to v16 (#1336)
* Pull in jslib

* Bump engines required to node 16 and npm 8

* Bump @types/node to 16

* Modify build.yml to build with node 16 and npm 8

* Update requirements in README.md

* Removed step to install npm8
npm8 is included in node v16

* Pull jslib
2021-12-13 17:16:57 +01:00
Thomas Rittson
56e2c86a7f Fix name for compatibility with config.js (#1334) 2021-12-10 18:10:20 +10:00
github-actions[bot]
8fba2a693e Autosync the updated translations (#1333)
Co-authored-by: github-actions <>
2021-12-10 01:18:21 +01:00
Oscar Hinton
f582d3e7a6 Bump angular to v12 (#1325) 2021-12-09 22:12:53 +01:00
Vince Grassia
75984a2e37 Remove old 'release' ref in workflow (#1328) 2021-12-07 22:49:27 -05:00
github-actions[bot]
1cba6dc3b9 Bumped version to 2.25.0 (#1327)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2021-12-07 19:46:24 -07:00
Oscar Hinton
a803d58c52 Fix components trying to use anglar bradcastservice vs interface (#1326) 2021-12-07 23:16:47 +01:00
Oscar Hinton
d5c0783619 Replace toaster library (#1322) 2021-12-07 20:41:45 +01:00
github-actions[bot]
35a7d6434a Autosync the updated translations (#1318)
Co-authored-by: github-actions <>
2021-12-07 17:06:08 +01:00
Oscar Hinton
78942cabf2 BEEEP: Refactor services DI (#1313) 2021-12-03 02:32:58 +00:00
Matt Gibson
d9231ae3f3 Fix families sponsorship redeem page (#1321)
* Display sponsorship warning when sponsoring an org

Move actions to drop down menu

Fix revoke cancel success popup

* Only show warning when sponsorship exists
2021-12-01 19:48:05 -06:00
Micaiah Martin
bca7c14319 Create workflow to automatically bump version (#1320) 2021-12-01 12:45:06 -07:00
Thomas Rittson
221931ecaa Update jslib (#1319)
* Update jslib

* Update constructors

* Update jslib
2021-11-29 10:14:49 +10:00
Justin Baur
4b856d9016 Fix basePrice to reflect the sponsorship (#1311)
* Fix basePrice to reflect the sponsorship

* Ran linter

* Add latest copy

* Remove unneeded if

* Fix times

* Stopped hardcoding basePrice

* Stopped hardcoding 40 in UI

* Switch to single small block

* Update jslib

* Revert "Update jslib"

This reverts commit 28534f2230.

* Revert "Remove unneeded if"

This reverts commit 5540b19998.

* Fix revert issue
2021-11-24 16:12:06 -05:00
Matt Gibson
4029554658 Fix formatting and title of sponsoring org drop down (#1317) 2021-11-24 14:41:57 -06:00
Matt Gibson
6ec22a9408 Add sponsorship pre validate to families redeem page (#1315)
* Add sponsorship pre validate to families redeem page

* Update messaging

* update jslib
2021-11-24 14:31:16 -06:00
Matt Gibson
9cc7dfb884 Force sponsorship friendly name to recipient address (#1316) 2021-11-24 13:38:38 -06:00
Oscar Hinton
dca12def8d Run npm lint in CI (#1314) 2021-11-24 19:23:31 +01:00
Arun Pattni
cbf65c5f42 Use 2fa.directory API v3 in inactive 2FA report (#1103)
* Use 2fa.directory API v3 in inactive 2FA report

* Fix issues

* Fix lint error

* Apply suggestions from code review

* Apply style suggestions

* Style fixes
2021-11-24 16:25:01 +01:00
Matt Gibson
f8c943c042 Display sponsored status for sponsored org subscription (#1312)
* Display sponsored status for sponsored org subscription

* Linter fixes
2021-11-24 08:33:34 -06:00
523 changed files with 51052 additions and 39216 deletions

View File

@@ -1,3 +1,4 @@
*
!docker/*
!build/*
!entrypoint.sh

View File

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

2
.git-blame-ignore-revs Normal file
View File

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

4
.gitattributes vendored
View File

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

View File

@@ -1,4 +1,5 @@
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
@@ -6,27 +7,26 @@
- [ ] 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
- **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
push:
branches-ignore:
- 'l10n_master'
- 'gh-pages'
- "l10n_master"
- "gh-pages"
jobs:
cloc:
@@ -28,7 +28,6 @@ jobs:
- name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
setup:
name: Setup
runs-on: ubuntu-20.04
@@ -40,9 +39,7 @@ jobs:
- name: Get GitHub sha as 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:
name: Build OSS zip
@@ -54,17 +51,13 @@ jobs:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
node-version: "16"
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment
@@ -95,7 +88,6 @@ jobs:
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
if-no-files-found: error
build-cloud:
name: Build Cloud zip
runs-on: ubuntu-20.04
@@ -106,17 +98,13 @@ jobs:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
node-version: "16"
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment
@@ -147,7 +135,6 @@ jobs:
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
if-no-files-found: error
build-commercial-selfhost:
name: Build SelfHost Docker image
runs-on: ubuntu-20.04
@@ -158,17 +145,13 @@ jobs:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
node-version: "16"
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment
@@ -182,7 +165,7 @@ jobs:
echo "GitHub event: $GITHUB_EVENT"
- name: Setup DCT
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/release'
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
id: setup-dct
uses: bitwarden/gh-actions/setup-docker-trust@a8c384a05a974c05c48374c818b004be221d43ff
with:
@@ -228,12 +211,12 @@ jobs:
if: github.ref == 'refs/heads/master'
run: docker tag bitwarden/web bitwarden/web:dev
- name: Tag release branch
if: github.ref == 'refs/heads/release'
run: docker tag bitwarden/web bitwarden/web:latest
- name: Tag hotfix branch
if: github.ref == 'refs/heads/hotfix'
run: docker tag bitwarden/web bitwarden/web:hotfix
- name: List Docker images
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/release'
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
run: docker images
- name: Push rc image
@@ -250,18 +233,17 @@ jobs:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
- name: Push latest image
if: github.ref == 'refs/heads/release'
run: docker push bitwarden/web:latest
- name: Push hotfix image
if: github.ref == 'refs/heads/hotfix'
run: docker push bitwarden/web:hotfix
env:
DOCKER_CONTENT_TRUST: 1
DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE: ${{ steps.setup-dct.outputs.dct-delegate-repo-passphrase }}
- name: Log out of Docker
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/release'
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
run: docker logout
build-qa:
name: Build Docker images for QA environment
runs-on: ubuntu-20.04
@@ -269,17 +251,13 @@ jobs:
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
node-version: "16"
- name: Cache npm
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment
@@ -329,7 +307,7 @@ jobs:
- name: Get image tag
id: image-tag
run: |
IMAGE_TAG=$(echo "$GITHUB_REF" | awk '{split($0, a, "/"); print a[3];}')
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
TAG_EXTENSION=${{ github.event.inputs.custom_tag_extension }}
if [[ $TAG_EXTENSION ]]; then
@@ -361,7 +339,6 @@ jobs:
- name: Log out of Docker
run: docker logout
windows:
name: Test code on Windows
runs-on: windows-2019
@@ -369,7 +346,7 @@ jobs:
- name: Set up NuGet
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
with:
nuget-version: 'latest'
nuget-version: "latest"
- name: Set up MSBuild
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
@@ -378,17 +355,13 @@ jobs:
id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with:
path: '~/.npm'
path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with:
node-version: '14'
- name: Update NPM
run: |
npm install -g npm@7
node-version: "16"
- name: Print environment
run: |
@@ -409,13 +382,12 @@ jobs:
- name: Install dependencies
run: npm ci
- name: NPM install
run: npm ci
- name: Run linter
run: npm run lint
- name: NPM build
run: npm run build:bit:cloud
crowdin-push:
name: Crowdin Push
if: github.ref == 'refs/heads/master'
@@ -454,7 +426,6 @@ jobs:
upload_sources: true
upload_translations: false
check-failures:
name: Check for failures
if: always()

View File

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

View File

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

View File

@@ -3,7 +3,15 @@ name: Release
on:
workflow_dispatch:
inputs: {}
inputs:
release_type:
description: 'Release Options'
required: true
default: 'Initial Release'
type: choice
options:
- Initial Release
- Redeploy
jobs:
setup:
@@ -34,7 +42,8 @@ jobs:
curl -sL https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | jq -r ".tag_name"
)
if [ "v$version" == "$previous_release_tag_version" ]; then
if [ "v$version" == "$previous_release_tag_version" ] && \
[ "${{ github.event.inputs.release_type }}" == "Initial Release" ]; then
echo "[!] Already released v$version. Please bump version to continue"
exit 1
fi
@@ -48,12 +57,12 @@ jobs:
BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME"
self-host:
name: Release self-host docker
runs-on: ubuntu-20.04
needs: setup
env:
_BRANCH_NAME: ${{ needs.setup.outputs.branch-name }}
_RELEASE_VERSION: ${{ needs.setup.outputs.release_version }}
steps:
- name: Print environment
@@ -73,27 +82,28 @@ jobs:
- name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f
- name: Pull latest selfhost Release image
run: docker pull bitwarden/web:latest
- name: Pull latest selfhost image
run: docker pull bitwarden/web:$_BRANCH_NAME
- name: Tag version
- name: Tag version and latest
run: |
docker tag bitwarden/web:latest bitwarden/web:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:$_RELEASE_VERSION
docker tag bitwarden/web:$_BRANCH_NAME bitwarden/web:latest
- name: List Docker images
run: docker images
- name: Push images
run: |
docker push bitwarden/web:$_RELEASE_VERSION
- name: Push version and latest image
env:
DOCKER_CONTENT_TRUST: 1
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
run: docker logout
ghpages-deploy:
name: Deploy Web Vault
runs-on: ubuntu-20.04
@@ -116,8 +126,6 @@ jobs:
- name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
with:
ref: release
- name: Setup git config
run: |
@@ -158,7 +166,6 @@ jobs:
--base gh-pages \
--head "$PR_BRANCH"
release:
name: Create GitHub Release
runs-on: ubuntu-20.04
@@ -174,7 +181,7 @@ jobs:
workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }}
artifacts: "web-*-selfhosted-COMMERCIAL.zip,
web-*-selfhosted-open-source.zip"
web-*-selfhosted-open-source.zip"
- name: Rename assets
run: |
@@ -189,6 +196,6 @@ jobs:
tag: "${{ needs.setup.outputs.tag_version }}"
body: "<insert release notes here>"
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 }}
draft: true

71
.github/workflows/version-bump.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
---
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 }}"

4
.husky/pre-commit Normal file
View File

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

12
.prettierignore Normal file
View File

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

3
.prettierrc.json Normal file
View File

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

View File

@@ -6,17 +6,12 @@ Please visit our [Community Forums](https://community.bitwarden.com/) for genera
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
* **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 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
- **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 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
@@ -24,9 +19,9 @@ Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web)
## Pull Request Guidelines
* use `npm run lint` and fix any linting suggestions before submitting a pull request
* commit any pull requests against the `master` branch
* include a link to your Community Forums post
- use `npm run lint` and fix any linting suggestions before submitting a pull request
- commit any pull requests against the `master` branch
- include a link to your Community Forums post
# Localization (l10n)

View File

@@ -1,4 +1,4 @@
FROM bitwarden/server
FROM nginx:stable
LABEL com.bitwarden.product="bitwarden"
@@ -8,13 +8,29 @@ RUN apt-get update \
curl \
&& rm -rf /var/lib/apt/lists/*
ENV ASPNETCORE_URLS http://+:5000
COPY docker/nginx.conf /etc/nginx
COPY docker/nginx-web.conf /etc/nginx
COPY docker/mime.types /etc/nginx
COPY docker/security-headers.conf /etc/nginx
WORKDIR /app
EXPOSE 5000
COPY ./build .
COPY entrypoint.sh /
COPY docker/entrypoint.sh /
RUN chmod +x /entrypoint.sh
HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1
RUN bash /entrypoint.sh
RUN chown -R bitwarden:bitwarden /app && chmod -R 755 /app && \
chown -R bitwarden:bitwarden /var/cache/nginx && \
chown -R bitwarden:bitwarden /var/log/nginx && \
chown -R bitwarden:bitwarden /etc/nginx/conf.d
RUN touch /var/run/nginx.pid && \
chown -R bitwarden:bitwarden /var/run/nginx.pid
ENTRYPOINT ["/entrypoint.sh"]
USER bitwarden
EXPOSE 8080
HEALTHCHECK CMD curl -f http://localhost:8080 || exit 1
#ENTRYPOINT ["/entrypoint.sh"]
#CMD ["tail", "-f", "/dev/null"]
CMD nginx -g 'daemon off;'

View File

@@ -23,8 +23,8 @@
### Requirements
- [Node.js](https://nodejs.org) v14.17 or greater
- NPM v7
- [Node.js](https://nodejs.org) v16.13.1 or greater
- NPM v8
### Run the app
@@ -48,16 +48,14 @@ You can also manually adjusting your API endpoint settings by adding `config/loc
```json
{
"dev": {
"proxyApi": "http://your-api-url",
"proxyIdentity": "http://your-identity-url",
"proxyEvents": "http://your-events-url",
"proxyNotifications": "http://your-notifications-url",
"allowedHosts": ["hostnames-to-allow-in-webpack"],
},
"urls": {
}
"dev": {
"proxyApi": "http://your-api-url",
"proxyIdentity": "http://your-identity-url",
"proxyEvents": "http://your-events-url",
"proxyNotifications": "http://your-notifications-url",
"allowedHosts": ["hostnames-to-allow-in-webpack"]
},
"urls": {}
}
```
@@ -68,3 +66,23 @@ 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.
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 { RouterModule, Routes } from '@angular/router';
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
const routes: Routes = [
{
path: 'providers',
loadChildren: async () => (await import('./providers/providers.module')).ProvidersModule,
},
{
path: "providers",
loadChildren: async () => (await import("./providers/providers.module")).ProvidersModule,
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class AppRoutingModule { }
export class AppRoutingModule {}

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import 'bootstrap';
import 'jquery';
import 'popper.js';
import "bootstrap";
import "jquery";
import "popper.js";
// 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') {
enableProdMode();
if (process.env.NODE_ENV === "production") {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });

View File

@@ -1,363 +1,492 @@
<div class="page-header d-flex">
<h1>{{'singleSignOn' | i18n}}</h1>
<h1>{{ "singleSignOn" | i18n }}</h1>
</div>
<ng-container *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<form #form (ngSubmit)="submit()" [formGroup]="data" [appApiAction]="formPromise" *ngIf="!loading" ngNativeValidate>
<p>
{{'ssoPolicyHelpStart' | i18n}}
<a routerLink="../policies">{{'ssoPolicyHelpLink' | i18n}}</a>
{{'ssoPolicyHelpEnd' | i18n}}
<br>
{{'ssoPolicyHelpKeyConnector' | i18n}}
</p>
<form
#form
(ngSubmit)="submit()"
[formGroup]="data"
[appApiAction]="formPromise"
*ngIf="!loading"
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>
<small class="form-text text-muted">{{'allowSsoDesc' | i18n}}</small>
<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>
<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 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="bwi bwi-question-circle" 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">
<label for="keyConnectorUrl">{{'keyConnectorUrl' | i18n}}</label>
<div class="input-group">
<input class="form-control" formControlName="keyConnectorUrl" 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>
<ng-container *ngIf="data.value.keyConnectorEnabled">
<app-callout type="warning" [useAlertRole]="true">
{{ "keyConnectorWarning" | i18n }}
</app-callout>
<div class="form-group">
<label for="type">{{'type' | i18n}}</label>
<select class="form-control" id="type" formControlName="configType">
<option value="0" disabled>{{'selectType' | i18n}}</option>
<option value="1">OpenID Connect</option>
<option value="2">SAML 2.0</option>
<label for="keyConnectorUrl">{{ "keyConnectorUrl" | i18n }}</label>
<div class="input-group">
<input
class="form-control"
formControlName="keyConnectorUrl"
id="keyConnectorUrl"
required
/>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
(click)="validateKeyConnectorUrl()"
[disabled]="!enableTestKeyConnector"
>
<i
class="bwi bwi-spinner bwi-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="bwi bwi-exclamation-circle" aria-hidden="true"></i>
{{ "keyConnectorTestFail" | i18n }}
</div>
<div class="text-success" *ngIf="!keyConnectorUrl.hasError('invalidUrl')" role="alert">
<i class="bwi bwi-check-circle" 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="bwi bwi-lg bwi-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="bwi bwi-lg bwi-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>
</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="bwi bwi-lg bwi-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="bwi bwi-lg bwi-external-link" aria-hidden="true"></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'copyValue' | i18n }}"
(click)="copy(spMetadataUrl)"
>
<i class="bwi bwi-lg bwi-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="bwi bwi-lg bwi-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>
<!-- 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 value="0">Redirect GET</option>
<option value="1">Form POST</option>
</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>
<!-- SAML2 IDP -->
<div class="config-section">
<h2>{{ "samlIdpConfig" | i18n }}</h2>
<div class="form-group">
<label for="idpEntityId">{{ "idpEntityId" | i18n }}</label>
<input class="form-control" formControlName="idpEntityId" id="idpEntityId" />
</div>
<div class="form-group">
<label for="idpBindingType">{{ "idpBindingType" | i18n }}</label>
<select class="form-control" formControlName="idpBindingType" id="idpBindingType">
<option value="1">Redirect</option>
<option value="2">HTTP POST</option>
<option value="4">Artifact</option>
</select>
</div>
<div class="form-group">
<label for="idpSingleSignOnServiceUrl">{{ "idpSingleSignOnServiceUrl" | i18n }}</label>
<input
class="form-control"
formControlName="idpSingleSignOnServiceUrl"
id="idpSingleSignOnServiceUrl"
/>
</div>
<div class="form-group">
<label for="idpSingleLogoutServiceUrl">{{ "idpSingleLogoutServiceUrl" | i18n }}</label>
<input
class="form-control"
formControlName="idpSingleLogoutServiceUrl"
id="idpSingleLogoutServiceUrl"
/>
</div>
<div class="form-group">
<label for="idpArtifactResolutionServiceUrl">{{
"idpArtifactResolutionServiceUrl" | i18n
}}</label>
<input
class="form-control"
formControlName="idpArtifactResolutionServiceUrl"
id="idpArtifactResolutionServiceUrl"
/>
</div>
<div class="form-group">
<label for="idpX509PublicCert">{{ "idpX509PublicCert" | i18n }}</label>
<textarea
formControlName="idpX509PublicCert"
class="form-control form-control-sm text-monospace"
rows="6"
id="idpX509PublicCert"
></textarea>
</div>
<div class="form-group">
<label for="idpOutboundSigningAlgorithm">{{ "idpOutboundSigningAlgorithm" | i18n }}</label>
<select
class="form-control"
formControlName="idpOutboundSigningAlgorithm"
id="idpOutboundSigningAlgorithm"
>
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{ o }}</option>
</select>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="idpAllowUnsolicitedAuthnResponse"
formControlName="idpAllowUnsolicitedAuthnResponse"
/>
<label class="form-check-label" for="idpAllowUnsolicitedAuthnResponse">
{{ "idpAllowUnsolicitedAuthnResponse" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="idpDisableOutboundLogoutRequests"
formControlName="idpDisableOutboundLogoutRequests"
/>
<label class="form-check-label" for="idpDisableOutboundLogoutRequests">
{{ "idpDisableOutboundLogoutRequests" | i18n }}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="idpWantAuthnRequestsSigned"
formControlName="idpWantAuthnRequestsSigned"
/>
<label class="form-check-label" for="idpWantAuthnRequestsSigned">
{{ "idpWantAuthnRequestsSigned" | i18n }}
</label>
</div>
</div>
</div>
</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>
<!-- SAML2 IDP -->
<div class="config-section">
<h2>{{'samlIdpConfig' | i18n}}</h2>
<div class="form-group">
<label for="idpEntityId">{{'idpEntityId' | i18n}}</label>
<input class="form-control" formControlName="idpEntityId" id="idpEntityId">
</div>
<div class="form-group">
<label for="idpBindingType">{{'idpBindingType' | i18n}}</label>
<select class="form-control" formControlName="idpBindingType" id="idpBindingType">
<option value="1">Redirect</option>
<option value="2">HTTP POST</option>
<option value="4">Artifact</option>
</select>
</div>
<div class="form-group">
<label for="idpSingleSignOnServiceUrl">{{'idpSingleSignOnServiceUrl' | i18n}}</label>
<input class="form-control" formControlName="idpSingleSignOnServiceUrl" id="idpSingleSignOnServiceUrl">
</div>
<div class="form-group">
<label for="idpSingleLogoutServiceUrl">{{'idpSingleLogoutServiceUrl' | i18n}}</label>
<input class="form-control" formControlName="idpSingleLogoutServiceUrl" id="idpSingleLogoutServiceUrl">
</div>
<div class="form-group">
<label for="idpArtifactResolutionServiceUrl">{{'idpArtifactResolutionServiceUrl' | i18n}}</label>
<input class="form-control" formControlName="idpArtifactResolutionServiceUrl"
id="idpArtifactResolutionServiceUrl">
</div>
<div class="form-group">
<label for="idpX509PublicCert">{{'idpX509PublicCert' | i18n}}</label>
<textarea formControlName="idpX509PublicCert" class="form-control form-control-sm text-monospace"
rows="6" id="idpX509PublicCert"></textarea>
</div>
<div class="form-group">
<label for="idpOutboundSigningAlgorithm">{{'idpOutboundSigningAlgorithm' | i18n}}</label>
<select class="form-control" formControlName="idpOutboundSigningAlgorithm"
id="idpOutboundSigningAlgorithm">
<option *ngFor="let o of samlSigningAlgorithms" [ngValue]="o">{{o}}</option>
</select>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="idpAllowUnsolicitedAuthnResponse"
formControlName="idpAllowUnsolicitedAuthnResponse">
<label class="form-check-label" for="idpAllowUnsolicitedAuthnResponse">
{{'idpAllowUnsolicitedAuthnResponse' | i18n}}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="idpDisableOutboundLogoutRequests"
formControlName="idpDisableOutboundLogoutRequests">
<label class="form-check-label" for="idpDisableOutboundLogoutRequests">
{{'idpDisableOutboundLogoutRequests' | i18n}}
</label>
</div>
</div>
<div class="form-group">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="idpWantAuthnRequestsSigned"
formControlName="idpWantAuthnRequestsSigned">
<label class="form-check-label" for="idpWantAuthnRequestsSigned">
{{'idpWantAuthnRequestsSigned' | i18n}}
</label>
</div>
</div>
</div>
</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>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
</form>

View File

@@ -1,180 +1,183 @@
import {
Component,
OnInit,
} from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { Organization } from 'jslib-common/models/domain/organization';
import { Organization } from "jslib-common/models/domain/organization";
import { OrganizationSsoRequest } from 'jslib-common/models/request/organization/organizationSsoRequest';
import { OrganizationSsoRequest } from "jslib-common/models/request/organization/organizationSsoRequest";
@Component({
selector: 'app-org-manage-sso',
templateUrl: 'sso.component.html',
selector: "app-org-manage-sso",
templateUrl: "sso.component.html",
})
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",
];
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;
organizationId: string;
organization: Organization;
formPromise: Promise<any>;
loading = true;
organizationId: string;
organization: Organization;
formPromise: Promise<any>;
callbackPath: string;
signedOutCallbackPath: string;
spEntityId: string;
spMetadataUrl: string;
spAcsUrl: string;
callbackPath: string;
signedOutCallbackPath: string;
spEntityId: string;
spMetadataUrl: string;
spAcsUrl: string;
enabled = this.formBuilder.control(false);
data = this.formBuilder.group({
configType: [],
enabled = this.fb.control(false);
data = this.fb.group({
configType: [],
keyConnectorEnabled: [],
keyConnectorUrl: [],
keyConnectorEnabled: [],
keyConnectorUrl: [],
// OpenId
authority: [],
clientId: [],
clientSecret: [],
metadataAddress: [],
redirectBehavior: [],
getClaimsFromUserInfoEndpoint: [],
additionalScopes: [],
additionalUserIdClaimTypes: [],
additionalEmailClaimTypes: [],
additionalNameClaimTypes: [],
acrValues: [],
expectedReturnAcrValue: [],
// OpenId
authority: [],
clientId: [],
clientSecret: [],
metadataAddress: [],
redirectBehavior: [],
getClaimsFromUserInfoEndpoint: [],
additionalScopes: [],
additionalUserIdClaimTypes: [],
additionalEmailClaimTypes: [],
additionalNameClaimTypes: [],
acrValues: [],
expectedReturnAcrValue: [],
// SAML
spNameIdFormat: [],
spOutboundSigningAlgorithm: [],
spSigningBehavior: [],
spMinIncomingSigningAlgorithm: [],
spWantAssertionsSigned: [],
spValidateCertificates: [],
// SAML
spNameIdFormat: [],
spOutboundSigningAlgorithm: [],
spSigningBehavior: [],
spMinIncomingSigningAlgorithm: [],
spWantAssertionsSigned: [],
spValidateCertificates: [],
idpEntityId: [],
idpBindingType: [],
idpSingleSignOnServiceUrl: [],
idpSingleLogoutServiceUrl: [],
idpArtifactResolutionServiceUrl: [],
idpX509PublicCert: [],
idpOutboundSigningAlgorithm: [],
idpAllowUnsolicitedAuthnResponse: [],
idpDisableOutboundLogoutRequests: [],
idpWantAuthnRequestsSigned: [],
});
idpEntityId: [],
idpBindingType: [],
idpSingleSignOnServiceUrl: [],
idpSingleLogoutServiceUrl: [],
idpArtifactResolutionServiceUrl: [],
idpX509PublicCert: [],
idpOutboundSigningAlgorithm: [],
idpAllowUnsolicitedAuthnResponse: [],
idpDisableOutboundLogoutRequests: [],
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();
});
}
constructor(private fb: FormBuilder, private route: ActivatedRoute, private apiService: ApiService,
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private userService: UserService) { }
async load() {
this.organization = await this.organizationService.get(this.organizationId);
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => {
this.organizationId = params.organizationId;
await this.load();
});
this.data.patchValue(ssoSettings.data);
this.enabled.setValue(ssoSettings.enabled);
this.callbackPath = ssoSettings.urls.callbackPath;
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
}
async load() {
this.organization = await this.userService.getOrganization(this.organizationId);
const ssoSettings = await this.apiService.getOrganizationSso(this.organizationId);
this.formPromise = null;
}
this.data.patchValue(ssoSettings.data);
this.enabled.setValue(ssoSettings.enabled);
async postData() {
if (this.data.get("keyConnectorEnabled").value) {
await this.validateKeyConnectorUrl();
this.callbackPath = ssoSettings.urls.callbackPath;
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;
if (this.keyConnectorUrl.hasError("invalidUrl")) {
throw new Error(this.i18nService.t("keyConnectorTestFail"));
}
}
copy(value: string) {
this.platformUtilsService.copyToClipboard(value);
const request = new OrganizationSsoRequest();
request.enabled = this.enabled.value;
request.data = this.data.value;
return this.apiService.postOrganizationSso(this.organizationId, request);
}
async validateKeyConnectorUrl() {
if (this.keyConnectorUrl.pristine) {
return;
}
launchUri(url: string) {
this.platformUtilsService.launchUri(url);
this.keyConnectorUrl.markAsPending();
try {
await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
this.keyConnectorUrl.updateValueAndValidity();
} catch {
this.keyConnectorUrl.setErrors({
invalidUrl: true,
});
}
async submit() {
this.formPromise = this.postData();
this.keyConnectorUrl.markAsPristine();
}
try {
const response = await this.formPromise;
get enableTestKeyConnector() {
return (
this.data.get("keyConnectorEnabled").value &&
this.keyConnectorUrl != null &&
this.keyConnectorUrl.value !== ""
);
}
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 postData() {
if (this.data.get('keyConnectorEnabled').value) {
await this.validateKeyConnectorUrl();
if (this.keyConnectorUrl.hasError('invalidUrl')) {
throw new Error(this.i18nService.t('keyConnectorTestFail'));
}
}
const request = new OrganizationSsoRequest();
request.enabled = this.enabled.value;
request.data = this.data.value;
return this.apiService.postOrganizationSso(this.organizationId, request);
}
async validateKeyConnectorUrl() {
if (this.keyConnectorUrl.pristine) {
return;
}
this.keyConnectorUrl.markAsPending();
try {
await this.apiService.getKeyConnectorAlive(this.keyConnectorUrl.value);
this.keyConnectorUrl.updateValueAndValidity();
} catch {
this.keyConnectorUrl.setErrors({
invalidUrl: true,
});
}
this.keyConnectorUrl.markAsPristine();
}
get enableTestKeyConnector() {
return this.data.get('keyConnectorEnabled').value &&
this.keyConnectorUrl != null &&
this.keyConnectorUrl.value !== '';
}
get keyConnectorUrl() {
return this.data.get('keyConnectorUrl');
}
get keyConnectorUrl() {
return this.data.get("keyConnectorUrl");
}
}

View File

@@ -1,54 +1,54 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { NgModule } from "@angular/core";
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 { ManageComponent } from 'src/app/organizations/manage/manage.component';
import { OrganizationGuardService } from 'src/app/services/organization-guard.service';
import { OrganizationTypeGuardService } from 'src/app/services/organization-type-guard.service';
import { OrganizationLayoutComponent } from "src/app/layouts/organization-layout.component";
import { ManageComponent } from "src/app/organizations/manage/manage.component";
import { OrganizationGuardService } from "src/app/services/organization-guard.service";
import { OrganizationTypeGuardService } from "src/app/services/organization-type-guard.service";
import { SsoComponent } from './manage/sso.component';
import { SsoComponent } from "./manage/sso.component";
const routes: Routes = [
{
path: 'organizations/:organizationId',
component: OrganizationLayoutComponent,
canActivate: [AuthGuardService, OrganizationGuardService],
{
path: "organizations/:organizationId",
component: OrganizationLayoutComponent,
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: [
{
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: [
{
path: 'sso',
component: SsoComponent,
},
],
},
{
path: "sso",
component: SsoComponent,
},
],
},
},
],
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class OrganizationsRoutingModule { }
export class OrganizationsRoutingModule {}

View File

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

View File

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

View File

@@ -1,24 +1,26 @@
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Component } from "@angular/core";
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 { BasePolicy, BasePolicyComponent } from 'src/app/organizations/policies/base-policy.component';
import {
BasePolicy,
BasePolicyComponent,
} from "src/app/organizations/policies/base-policy.component";
export class DisablePersonalVaultExportPolicy extends BasePolicy {
name = 'disablePersonalVaultExport';
description = 'disablePersonalVaultExportDesc';
type = PolicyType.DisablePersonalVaultExport;
component = DisablePersonalVaultExportPolicyComponent;
name = "disablePersonalVaultExport";
description = "disablePersonalVaultExportDesc";
type = PolicyType.DisablePersonalVaultExport;
component = DisablePersonalVaultExportPolicyComponent;
}
@Component({
selector: 'policy-disable-personal-vault-export',
templateUrl: 'disable-personal-vault-export.component.html',
selector: "policy-disable-personal-vault-export",
templateUrl: "disable-personal-vault-export.component.html",
})
export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {
}
export class DisablePersonalVaultExportPolicyComponent extends BasePolicyComponent {}

View File

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

View File

@@ -1,70 +1,72 @@
import { Component } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Component } from "@angular/core";
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 { BasePolicy, BasePolicyComponent } from 'src/app/organizations/policies/base-policy.component';
import {
BasePolicy,
BasePolicyComponent,
} from "src/app/organizations/policies/base-policy.component";
export class MaximumVaultTimeoutPolicy extends BasePolicy {
name = 'maximumVaultTimeout';
description = 'maximumVaultTimeoutDesc';
type = PolicyType.MaximumVaultTimeout;
component = MaximumVaultTimeoutPolicyComponent;
name = "maximumVaultTimeout";
description = "maximumVaultTimeoutDesc";
type = PolicyType.MaximumVaultTimeout;
component = MaximumVaultTimeoutPolicyComponent;
}
@Component({
selector: 'policy-maximum-timeout',
templateUrl: 'maximum-vault-timeout.component.html',
selector: "policy-maximum-timeout",
templateUrl: "maximum-vault-timeout.component.html",
})
export class MaximumVaultTimeoutPolicyComponent extends BasePolicyComponent {
data = this.formBuilder.group({
hours: [null],
minutes: [null],
});
data = this.fb.group({
hours: [null],
minutes: [null],
constructor(private formBuilder: FormBuilder, private i18nService: I18nService) {
super();
}
loadData() {
const minutes = this.policyResponse.data?.minutes;
if (minutes == null) {
return;
}
this.data.patchValue({
hours: Math.floor(minutes / 60),
minutes: minutes % 60,
});
}
constructor(private fb: FormBuilder, private i18nService: I18nService) {
super();
buildRequestData() {
if (this.data.value.hours == null && this.data.value.minutes == null) {
return null;
}
loadData() {
const minutes = this.policyResponse.data?.minutes;
return {
minutes: this.data.value.hours * 60 + this.data.value.minutes,
};
}
if (minutes == null) {
return;
}
this.data.patchValue({
hours: Math.floor(minutes / 60),
minutes: minutes % 60,
});
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"));
}
buildRequestData() {
if (this.data.value.hours == null && this.data.value.minutes == null) {
return null;
}
return {
minutes: this.data.value.hours * 60 + this.data.value.minutes,
};
const data = this.buildRequestData();
if (data?.minutes == null || data?.minutes <= 0) {
throw new Error(this.i18nService.t("invalidMaximumVaultTimeout"));
}
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);
}
return super.buildRequest(policiesEnabledMap);
}
}

View File

@@ -1,35 +1,46 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="addTitle">
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="addTitle">
{{'addExistingOrganization' | i18n}}
</h2>
<button type="button" 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>
</td>
</tr>
</table>
</ng-container>
</div>
<div class="modal-dialog modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="addTitle">
{{ "addExistingOrganization" | i18n }}
</h2>
<button
type="button"
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="bwi bwi-spinner bwi-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>
</td>
</tr>
</table>
</ng-container>
</div>
</div>
</div>
</div>

View File

@@ -1,83 +1,86 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { ValidationService } from 'jslib-angular/services/validation.service';
import { ValidationService } from "jslib-angular/services/validation.service";
import { ProviderService } from '../services/provider.service';
import { WebProviderService } from "../services/webProvider.service";
import { Organization } from 'jslib-common/models/domain/organization';
import { Provider } from 'jslib-common/models/domain/provider';
import { PlanType } from 'jslib-common/enums/planType';
import { Organization } from "jslib-common/models/domain/organization";
import { Provider } from "jslib-common/models/domain/provider";
@Component({
selector: 'provider-add-organization',
templateUrl: 'add-organization.component.html',
selector: "provider-add-organization",
templateUrl: "add-organization.component.html",
})
export class AddOrganizationComponent implements OnInit {
@Input() providerId: string;
@Input() organizations: Organization[];
@Output() onAddedOrganization = new EventEmitter();
@Input() providerId: string;
@Input() organizations: Organization[];
@Output() onAddedOrganization = new EventEmitter();
provider: Provider;
formPromise: Promise<any>;
loading = true;
provider: Provider;
formPromise: Promise<any>;
loading = true;
constructor(
private providerService: ProviderService,
private webProviderService: WebProviderService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private validationService: ValidationService
) {}
constructor(private userService: UserService, private providerService: ProviderService,
private toasterService: ToasterService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private validationService: ValidationService,
private apiService: ApiService) { }
async ngOnInit() {
await this.load();
}
async ngOnInit() {
await this.load();
async load() {
if (this.providerId == null) {
return;
}
async load() {
if (this.providerId == null) {
return;
}
this.provider = await this.providerService.get(this.providerId);
this.provider = await this.userService.getProvider(this.providerId);
this.loading = false;
}
this.loading = false;
async add(organization: Organization) {
if (this.formPromise) {
return;
}
async add(organization: Organization) {
if (this.formPromise) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("addOrganizationConfirmation", organization.name, this.provider.name),
organization.name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
const confirmed = await this.platformUtilsService.showDialog(
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();
if (!confirmed) {
return false;
}
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,62 +1,90 @@
<div class="page-header d-flex">
<h1>{{'clients' | i18n}}</h1>
<h1>{{ "clients" | i18n }}</h1>
<div class="ml-auto d-flex">
<div>
<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>
<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 class="ml-auto d-flex">
<div>
<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>
<a class="btn btn-sm btn-outline-primary ml-3" routerLink="create" *ngIf="manageOrganizations">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "newClientOrganization" | i18n }}
</a>
<button
class="btn btn-sm btn-outline-primary ml-3"
(click)="addExistingOrganization()"
*ngIf="manageOrganizations && showAddExisting"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
{{ "addExistingOrganization" | i18n }}
</button>
</div>
</div>
<ng-container *ngIf="loading">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container
*ngIf="!loading && (clients | search:searchText:'organizationName':'id') as searchedClients">
<p *ngIf="!searchedClients.length">{{'noClientsInList' | i18n}}</p>
<ng-container *ngIf="searchedClients.length">
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<tbody>
<tr *ngFor="let o of searchedClients">
<td width="30">
<app-avatar [data]="o.organizationName" size="25" [circle]="true" [fontSize]="14"></app-avatar>
</td>
<td>
<a [routerLink]="['/organizations', o.organizationId]">{{o.organizationName}}</a>
</td>
<td class="table-list-options" *ngIf="manageOrganizations">
<div class="dropdown" appListDropdown>
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
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>
*ngIf="!loading && (clients | search: searchText:'organizationName':'id') as searchedClients"
>
<p *ngIf="!searchedClients.length">{{ "noClientsInList" | i18n }}</p>
<ng-container *ngIf="searchedClients.length">
<table
class="table table-hover table-list"
infiniteScroll
[infiniteScrollDistance]="1"
[infiniteScrollDisabled]="!isPaging()"
(scrolled)="loadMore()"
>
<tbody>
<tr *ngFor="let o of searchedClients">
<td width="30">
<app-avatar
[data]="o.organizationName"
size="25"
[circle]="true"
[fontSize]="14"
></app-avatar>
</td>
<td>
<a [routerLink]="['/organizations', o.organizationId]">{{ o.organizationName }}</a>
</td>
<td class="table-list-options" *ngIf="manageOrganizations">
<div class="dropdown" appListDropdown>
<button
class="btn btn-outline-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
appA11yTitle="{{ 'options' | i18n }}"
>
<i class="bwi bwi-cog bwi-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="bwi bwi-fw bwi-close" aria-hidden="true"></i>
{{ "remove" | i18n }}
</a>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</ng-container>
</ng-container>
<ng-template #add></ng-template>

View File

@@ -1,159 +1,183 @@
import {
Component,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from 'rxjs/operators';
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 { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { ModalService } from 'jslib-angular/services/modal.service';
import { ValidationService } from 'jslib-angular/services/validation.service';
import { ModalService } from "jslib-angular/services/modal.service";
import { ValidationService } from "jslib-angular/services/validation.service";
import { PlanType } from 'jslib-common/enums/planType';
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { PlanType } from "jslib-common/enums/planType";
import { ProviderUserType } from "jslib-common/enums/providerUserType";
import { Organization } from 'jslib-common/models/domain/organization';
import {
ProviderOrganizationOrganizationDetailsResponse
} from 'jslib-common/models/response/provider/providerOrganizationResponse';
import { Organization } from "jslib-common/models/domain/organization";
import { ProviderOrganizationOrganizationDetailsResponse } from "jslib-common/models/response/provider/providerOrganizationResponse";
import { ProviderService } from '../services/provider.service';
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({
templateUrl: 'clients.component.html',
templateUrl: "clients.component.html",
})
export class ClientsComponent implements OnInit {
@ViewChild("add", { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
@ViewChild('add', { read: ViewContainerRef, static: true }) addModalRef: ViewContainerRef;
providerId: any;
searchText: string;
addableOrganizations: Organization[];
loading = true;
manageOrganizations = false;
showAddExisting = false;
providerId: any;
searchText: string;
addableOrganizations: Organization[];
loading = true;
manageOrganizations = false;
showAddExisting = false;
clients: ProviderOrganizationOrganizationDetailsResponse[];
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
clients: ProviderOrganizationOrganizationDetailsResponse[];
pagedClients: ProviderOrganizationOrganizationDetailsResponse[];
protected didScroll = false;
protected pageSize = 100;
protected actionPromise: Promise<any>;
private pagedClientsCount = 0;
protected didScroll = false;
protected pageSize = 100;
protected actionPromise: Promise<any>;
private pagedClientsCount = 0;
constructor(
private route: ActivatedRoute,
private providerService: ProviderService,
private apiService: ApiService,
private searchService: SearchService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private validationService: ValidationService,
private webProviderService: WebProviderService,
private logService: LogService,
private modalService: ModalService,
private organizationService: OrganizationService
) {}
constructor(private route: ActivatedRoute, private userService: UserService,
private apiService: ApiService, private searchService: SearchService,
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
private toasterService: ToasterService, private validationService: ValidationService,
private providerService: ProviderService, private logService: LogService,
private modalService: ModalService) { }
async ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
async ngOnInit() {
this.route.parent.params.subscribe(async params => {
this.providerId = params.providerId;
await this.load();
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();
this.route.queryParams.pipe(first()).subscribe(async qParams => {
this.searchText = qParams.search;
});
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;
}
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.userService.getProvider(this.providerId)).type === ProviderUserType.ProviderAdmin;
const candidateOrgs = (await this.userService.getAllOrganizations()).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();
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 = this.webProviderService.detachOrganizastion(
this.providerId,
organization.id
);
try {
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("detachedOrganization", organization.organizationName)
);
await this.load();
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
}

View File

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

View File

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

View File

@@ -1,35 +1,42 @@
<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>
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-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 && !authed">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'joinProvider' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<p class="text-center">
{{providerName}}
<strong class="d-block mt-2">{{email}}</strong>
</p>
<p>{{'joinProviderDesc' | i18n}}</p>
<hr>
<div class="d-flex">
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{'logIn' | i18n}}
</a>
<a routerLink="/register" [queryParams]="{email: email}"
class="btn btn-primary btn-block ml-2 mt-0">
{{'createAccount' | i18n}}
</a>
</div>
</div>
</div>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "joinProvider" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<p class="text-center">
{{ providerName }}
<strong class="d-block mt-2">{{ email }}</strong>
</p>
<p>{{ "joinProviderDesc" | i18n }}</p>
<hr />
<div class="d-flex">
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
{{ "logIn" | i18n }}
</a>
<a
routerLink="/register"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block ml-2 mt-0"
>
{{ "createAccount" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

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

View File

@@ -1,38 +1,34 @@
import {
Component,
Input,
} from '@angular/core';
import { Component, Input } from "@angular/core";
import { ProviderUserBulkConfirmRequest } from 'jslib-common/models/request/provider/providerUserBulkConfirmRequest';
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { ProviderUserBulkConfirmRequest } from "jslib-common/models/request/provider/providerUserBulkConfirmRequest";
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 { BulkUserDetails } from 'src/app/organizations/manage/bulk/bulk-status.component';
import { BulkConfirmComponent as OrganizationBulkConfirmComponent } from "src/app/organizations/manage/bulk/bulk-confirm.component";
import { BulkUserDetails } from "src/app/organizations/manage/bulk/bulk-status.component";
@Component({
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 {
@Input() providerId: string;
@Input() providerId: string;
protected isAccepted(user: BulkUserDetails) {
return user.status === ProviderUserStatusType.Accepted;
}
protected isAccepted(user: BulkUserDetails) {
return user.status === ProviderUserStatusType.Accepted;
}
protected async getPublicKeys() {
const request = new ProviderUserBulkRequest(this.filteredUsers.map((user) => user.id));
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
}
protected async getPublicKeys() {
const request = new ProviderUserBulkRequest(this.filteredUsers.map(user => user.id));
return await this.apiService.postProviderUsersPublicKey(this.providerId, request);
}
protected getCryptoKey() {
return this.cryptoService.getProviderKey(this.providerId);
}
protected getCryptoKey() {
return this.cryptoService.getProviderKey(this.providerId);
}
protected async postConfirmRequest(userIdsWithKeys: any[]) {
const request = new ProviderUserBulkConfirmRequest(userIdsWithKeys);
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,21 +1,17 @@
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({
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 {
@Input() providerId: string;
@Input() providerId: string;
async deleteUsers() {
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,68 +1,107 @@
<div class="page-header d-flex">
<h1>{{'eventLogs' | i18n}}</h1>
<div class="ml-auto d-flex">
<div class="form-inline">
<label class="sr-only" for="start">{{'startDate' | i18n}}</label>
<input type="datetime-local" class="form-control form-control-sm" id="start"
placeholder="{{'startDate' | i18n}}" [(ngModel)]="start" placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true">
<span class="mx-2">-</span>
<label class="sr-only" for="end">{{'endDate' | i18n}}</label>
<input type="datetime-local" class="form-control form-control-sm" id="end"
placeholder="{{'endDate' | i18n}}" [(ngModel)]="end" placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true">
</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>
<h1>{{ "eventLogs" | i18n }}</h1>
<div class="ml-auto d-flex">
<div class="form-inline">
<label class="sr-only" for="start">{{ "startDate" | i18n }}</label>
<input
type="datetime-local"
class="form-control form-control-sm"
id="start"
placeholder="{{ 'startDate' | i18n }}"
[(ngModel)]="start"
placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true"
/>
<span class="mx-2">-</span>
<label class="sr-only" for="end">{{ "endDate" | i18n }}</label>
<input
type="datetime-local"
class="form-control form-control-sm"
id="end"
placeholder="{{ 'endDate' | i18n }}"
[(ngModel)]="end"
placeholder="YYYY-MM-DDTHH:MM"
(change)="dirtyDates = true"
/>
</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="bwi bwi-refresh bwi-fw"
aria-hidden="true"
[ngClass]="{ 'bwi-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="bwi bwi-spinner bwi-spin" aria-hidden="true"></i>
<span>{{ "export" | i18n }}</span>
</button>
</form>
</div>
</div>
<ng-container *ngIf="!loaded">
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
<span class="sr-only">{{'loading' | i18n}}</span>
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<ng-container *ngIf="loaded">
<p *ngIf="!events || !events.length">{{'noEventsInList' | i18n}}</p>
<table class="table table-hover" *ngIf="events && events.length">
<thead>
<tr>
<th class="border-top-0" width="210">{{'timestamp' | i18n}}</th>
<th class="border-top-0" width="40">
<span class="sr-only">{{'device' | i18n}}</span>
</th>
<th class="border-top-0" width="150">{{'user' | i18n}}</th>
<th class="border-top-0">{{'event' | i18n}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let e of events">
<td>{{e.date | date:'medium'}}</td>
<td>
<i class="text-muted fa fa-lg {{e.appIcon}}" title="{{e.appName}}, {{e.ip}}" aria-hidden="true"></i>
<span class="sr-only">{{e.appName}}, {{e.ip}}</span>
</td>
<td>
<span title="{{e.userEmail}}">{{e.userName}}</span>
</td>
<td [innerHTML]="e.message"></td>
</tr>
</tbody>
</table>
<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>
<p *ngIf="!events || !events.length">{{ "noEventsInList" | i18n }}</p>
<table class="table table-hover" *ngIf="events && events.length">
<thead>
<tr>
<th class="border-top-0" width="210">{{ "timestamp" | i18n }}</th>
<th class="border-top-0" width="40">
<span class="sr-only">{{ "device" | i18n }}</span>
</th>
<th class="border-top-0" width="150">{{ "user" | i18n }}</th>
<th class="border-top-0">{{ "event" | i18n }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let e of events">
<td>{{ e.date | date: "medium" }}</td>
<td>
<i
class="text-muted bwi bwi-lg {{ e.appIcon }}"
title="{{ e.appName }}, {{ e.ip }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ e.appName }}, {{ e.ip }}</span>
</td>
<td>
<span title="{{ e.userEmail }}">{{ e.userName }}</span>
</td>
<td [innerHTML]="e.message"></td>
</tr>
</tbody>
</table>
<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="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "loadMore" | i18n }}</span>
</button>
</ng-container>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,237 +1,292 @@
import {
Component,
OnInit,
ViewChild,
ViewContainerRef
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from 'rxjs/operators';
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 { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { SearchService } from "jslib-common/abstractions/search.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { ModalService } from 'jslib-angular/services/modal.service';
import { ValidationService } from 'jslib-angular/services/validation.service';
import { ModalService } from "jslib-angular/services/modal.service";
import { ValidationService } from "jslib-angular/services/validation.service";
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType';
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType";
import { ProviderUserType } from "jslib-common/enums/providerUserType";
import { SearchPipe } from 'jslib-angular/pipes/search.pipe';
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe';
import { SearchPipe } from "jslib-angular/pipes/search.pipe";
import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
import { ListResponse } from 'jslib-common/models/response/listResponse';
import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse';
import { ListResponse } from "jslib-common/models/response/listResponse";
import { ProviderUserUserDetailsResponse } from "jslib-common/models/response/provider/providerUserResponse";
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest';
import { ProviderUserConfirmRequest } from 'jslib-common/models/request/provider/providerUserConfirmRequest';
import { ProviderUserBulkResponse } from 'jslib-common/models/response/provider/providerUserBulkResponse';
import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
import { ProviderUserConfirmRequest } from "jslib-common/models/request/provider/providerUserConfirmRequest";
import { ProviderUserBulkResponse } from "jslib-common/models/response/provider/providerUserBulkResponse";
import { BasePeopleComponent } from 'src/app/common/base.people.component';
import { BulkStatusComponent } from 'src/app/organizations/manage/bulk/bulk-status.component';
import { EntityEventsComponent } from 'src/app/organizations/manage/entity-events.component';
import { BulkConfirmComponent } from './bulk/bulk-confirm.component';
import { BulkRemoveComponent } from './bulk/bulk-remove.component';
import { UserAddEditComponent } from './user-add-edit.component';
import { BasePeopleComponent } from "src/app/common/base.people.component";
import { BulkStatusComponent } from "src/app/organizations/manage/bulk/bulk-status.component";
import { EntityEventsComponent } from "src/app/organizations/manage/entity-events.component";
import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
import { UserAddEditComponent } from "./user-add-edit.component";
@Component({
selector: 'provider-people',
templateUrl: 'people.component.html',
selector: "provider-people",
templateUrl: "people.component.html",
})
export class PeopleComponent extends BasePeopleComponent<ProviderUserUserDetailsResponse> implements OnInit {
export class PeopleComponent
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;
@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;
userStatusType = ProviderUserStatusType;
providerId: string;
accessEvents = false;
userType = ProviderUserType;
userStatusType = ProviderUserStatusType;
providerId: string;
accessEvents = false;
constructor(
apiService: ApiService,
private route: ActivatedRoute,
i18nService: I18nService,
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
);
}
constructor(apiService: ApiService, private route: ActivatedRoute,
i18nService: I18nService, modalService: ModalService,
platformUtilsService: PlatformUtilsService, toasterService: ToasterService,
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);
}
ngOnInit() {
this.route.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
const provider = await this.providerService.get(this.providerId);
ngOnInit() {
this.route.parent.params.subscribe(async params => {
this.providerId = params.providerId;
const provider = await this.userService.getProvider(this.providerId);
if (!provider.canManageUsers) {
this.router.navigate(["../"], { relativeTo: this.route });
return;
}
if (!provider.canManageUsers) {
this.router.navigate(['../'], { relativeTo: this.route });
return;
}
this.accessEvents = provider.useEvents;
this.accessEvents = provider.useEvents;
await this.load();
await this.load();
this.route.queryParams.pipe(first()).subscribe(async qParams => {
this.searchText = qParams.search;
if (qParams.viewEvents != null) {
const user = this.users.filter(u => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
this.events(user[0]);
}
}
});
});
}
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;
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
if (qParams.viewEvents != null) {
const user = this.users.filter((u) => u.id === qParams.viewEvents);
if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
this.events(user[0]);
}
}
});
});
}
const [modal] = await this.modalService.openViewRef(BulkRemoveComponent, this.bulkRemoveModalRef, comp => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
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);
});
}
);
}
await modal.onClosedPromise();
await this.load();
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;
}
async bulkReinvite() {
if (this.actionPromise != null) {
return;
}
const [modal] = await this.modalService.openViewRef(
BulkRemoveComponent,
this.bulkRemoveModalRef,
(comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
}
);
const users = this.getCheckedUsers();
const filteredUsers = users.filter(u => u.status === ProviderUserStatusType.Invited);
await modal.onClosedPromise();
await this.load();
}
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 bulkReinvite() {
if (this.actionPromise != null) {
return;
}
async bulkConfirm() {
if (this.actionPromise != null) {
return;
}
const users = this.getCheckedUsers();
const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited);
const [modal] = await this.modalService.openViewRef(BulkConfirmComponent, this.bulkConfirmModalRef, comp => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
});
await modal.onClosedPromise();
await this.load();
if (filteredUsers.length <= 0) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("noSelectedUsersApplicable")
);
return;
}
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;
}
} catch {
close = true;
modal.close();
}
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;
}
} catch {
close = true;
modal.close();
}
}
}

View File

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

View File

@@ -1,112 +1,121 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
} from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { ToasterService } from 'angular2-toaster';
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 { 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 { ProviderUserInviteRequest } from "jslib-common/models/request/provider/providerUserInviteRequest";
import { ProviderUserInviteRequest } from 'jslib-common/models/request/provider/providerUserInviteRequest';
import { PermissionsApi } from "jslib-common/models/api/permissionsApi";
import { PermissionsApi } from 'jslib-common/models/api/permissionsApi';
import { ProviderUserType } from 'jslib-common/enums/providerUserType';
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({
selector: 'provider-user-add-edit',
templateUrl: 'user-add-edit.component.html',
selector: "provider-user-add-edit",
templateUrl: "user-add-edit.component.html",
})
export class UserAddEditComponent implements OnInit {
@Input() name: string;
@Input() providerUserId: string;
@Input() providerId: string;
@Output() onSavedUser = new EventEmitter();
@Output() onDeletedUser = new EventEmitter();
@Input() name: string;
@Input() providerUserId: string;
@Input() providerId: string;
@Output() onSavedUser = new EventEmitter();
@Output() onDeletedUser = new EventEmitter();
loading = true;
editMode: boolean = false;
title: string;
emails: string;
type: ProviderUserType = ProviderUserType.ServiceUser;
permissions = new PermissionsApi();
showCustom = false;
access: 'all' | 'selected' = 'selected';
formPromise: Promise<any>;
deletePromise: Promise<any>;
userType = ProviderUserType;
loading = true;
editMode: boolean = false;
title: string;
emails: string;
type: ProviderUserType = ProviderUserType.ServiceUser;
permissions = new PermissionsApi();
showCustom = false;
access: "all" | "selected" = "selected";
formPromise: Promise<any>;
deletePromise: Promise<any>;
userType = ProviderUserType;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private platformUtilsService: PlatformUtilsService,
private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
this.editMode = this.loading = this.providerUserId != null;
async ngOnInit() {
this.editMode = this.loading = this.providerUserId != null;
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t('editUser');
try {
const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId);
this.type = user.type;
} catch (e) {
this.logService.error(e);
}
} else {
this.title = this.i18nService.t('inviteUser');
}
this.loading = false;
if (this.editMode) {
this.editMode = true;
this.title = this.i18nService.t("editUser");
try {
const user = await this.apiService.getProviderUser(this.providerId, this.providerUserId);
this.type = user.type;
} catch (e) {
this.logService.error(e);
}
} else {
this.title = this.i18nService.t("inviteUser");
}
async submit() {
try {
if (this.editMode) {
const request = new ProviderUserUpdateRequest();
request.type = this.type;
this.formPromise = this.apiService.putProviderUser(this.providerId, this.providerUserId, request);
} else {
const request = new ProviderUserInviteRequest();
request.emails = this.emails.trim().split(/\s*,\s*/);
request.type = this.type;
this.formPromise = this.apiService.postProviderUserInvite(this.providerId, request);
}
await this.formPromise;
this.toasterService.popAsync('success', null,
this.i18nService.t(this.editMode ? 'editedUserId' : 'invitedUsers', this.name));
this.onSavedUser.emit();
} catch (e) {
this.logService.error(e);
}
this.loading = false;
}
async submit() {
try {
if (this.editMode) {
const request = new ProviderUserUpdateRequest();
request.type = this.type;
this.formPromise = this.apiService.putProviderUser(
this.providerId,
this.providerUserId,
request
);
} else {
const request = new ProviderUserInviteRequest();
request.emails = this.emails.trim().split(/\s*,\s*/);
request.type = this.type;
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;
}
async delete() {
if (!this.editMode) {
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('removeUserConfirmation'), this.name,
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
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 (e) {
this.logService.error(e);
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("removeUserConfirmation"),
this.name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
}
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>
<div class="org-nav" *ngIf="provider">
<div class="container d-flex">
<div class="d-flex flex-column">
<div class="my-auto d-flex align-items-center pl-1">
<app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar>
<div class="org-name ml-3">
<span>{{provider.name}}</span>
<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 class="container d-flex">
<div class="d-flex flex-column">
<div class="my-auto d-flex align-items-center pl-1">
<app-avatar [data]="provider.name" size="45" [circle]="true"></app-avatar>
<div class="org-name ml-3">
<span>{{ provider.name }}</span>
<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="bwi bwi-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="bwi bwi-bank" aria-hidden="true"></i>
{{ "clients" | i18n }}
</a>
</li>
<li class="nav-item" *ngIf="showManageTab">
<a class="nav-link" [routerLink]="manageRoute" routerLinkActive="active">
<i class="bwi bwi-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="bwi bwi-cogs" aria-hidden="true"></i>
{{ "settings" | i18n }}
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="container page-content">
<router-outlet></router-outlet>
<router-outlet></router-outlet>
</div>
<app-footer></app-footer>

View File

@@ -1,51 +1,50 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { UserService } from 'jslib-common/abstractions/user.service';
import { ProviderService } from "jslib-common/abstractions/provider.service";
import { Provider } from 'jslib-common/models/domain/provider';
import { Provider } from "jslib-common/models/domain/provider";
@Component({
selector: 'providers-layout',
templateUrl: 'providers-layout.component.html',
selector: "providers-layout",
templateUrl: "providers-layout.component.html",
})
export class ProvidersLayoutComponent {
provider: Provider;
private providerId: string;
provider: Provider;
private providerId: string;
constructor(private route: ActivatedRoute, private providerService: ProviderService) {}
constructor(private route: ActivatedRoute, private userService: UserService) { }
ngOnInit() {
document.body.classList.remove("layout_frontend");
this.route.params.subscribe(async (params) => {
this.providerId = params.providerId;
await this.load();
});
}
ngOnInit() {
document.body.classList.remove('layout_frontend');
this.route.params.subscribe(async params => {
this.providerId = params.providerId;
await this.load();
});
}
async load() {
this.provider = await this.userService.getProvider(this.providerId);
}
get showMenuBar() {
return this.showManageTab || this.showSettingsTab;
}
get showManageTab() {
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
}
get showSettingsTab() {
return this.provider.isProviderAdmin;
}
get manageRoute(): string {
switch (true) {
case this.provider.canManageUsers:
return 'manage/people';
case this.provider.canAccessEventLogs:
return 'manage/events';
}
async load() {
this.provider = await this.providerService.get(this.providerId);
}
get showMenuBar() {
return this.showManageTab || this.showSettingsTab;
}
get showManageTab() {
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
}
get showSettingsTab() {
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 { RouterModule, Routes } from '@angular/router';
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuardService } from 'jslib-angular/services/auth-guard.service';
import { Permissions } from 'jslib-common/enums/permissions';
import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
import { Permissions } from "jslib-common/enums/permissions";
import { AddOrganizationComponent } from './clients/add-organization.component';
import { ClientsComponent } from './clients/clients.component';
import { CreateOrganizationComponent } from './clients/create-organization.component';
import { AcceptProviderComponent } from './manage/accept-provider.component';
import { EventsComponent } from './manage/events.component';
import { ManageComponent } from './manage/manage.component';
import { PeopleComponent } from './manage/people.component';
import { ProvidersLayoutComponent } from './providers-layout.component';
import { SettingsComponent } from './settings/settings.component';
import { SetupProviderComponent } from './setup/setup-provider.component';
import { SetupComponent } from './setup/setup.component';
import { AddOrganizationComponent } from "./clients/add-organization.component";
import { ClientsComponent } from "./clients/clients.component";
import { CreateOrganizationComponent } from "./clients/create-organization.component";
import { AcceptProviderComponent } from "./manage/accept-provider.component";
import { EventsComponent } from "./manage/events.component";
import { ManageComponent } from "./manage/manage.component";
import { PeopleComponent } from "./manage/people.component";
import { ProvidersLayoutComponent } from "./providers-layout.component";
import { SettingsComponent } from "./settings/settings.component";
import { SetupProviderComponent } from "./setup/setup-provider.component";
import { SetupComponent } from "./setup/setup.component";
import { 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 { ProviderGuardService } from './services/provider-guard.service';
import { ProviderTypeGuardService } from './services/provider-type-guard.service';
import { AccountComponent } from './settings/account.component';
import { ProvidersComponent } from "src/app/providers/providers.component";
import { ProviderGuardService } from "./services/provider-guard.service";
import { ProviderTypeGuardService } from "./services/provider-type-guard.service";
import { AccountComponent } from "./settings/account.component";
const routes: Routes = [
{
path: '',
canActivate: [AuthGuardService],
component: ProvidersComponent,
},
{
path: '',
component: FrontendLayoutComponent,
{
path: "",
canActivate: [AuthGuardService],
component: ProvidersComponent,
},
{
path: "",
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: [
{
path: 'setup-provider',
component: SetupProviderComponent,
data: { titleId: 'setupProvider' },
},
{
path: 'accept-provider',
component: AcceptProviderComponent,
data: { titleId: 'acceptProvider' },
},
{ 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],
},
},
],
},
],
},
{
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({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ProvidersRoutingModule { }
export class ProvidersRoutingModule {}

View File

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

View File

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

View File

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

View File

@@ -1,32 +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 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

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

View File

@@ -1,62 +1,65 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ToasterService } from 'angular2-toaster';
import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
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 { SyncService } from 'jslib-common/abstractions/sync.service';
import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { 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({
selector: 'provider-account',
templateUrl: 'account.component.html',
selector: "provider-account",
templateUrl: "account.component.html",
})
export class AccountComponent {
selfHosted = false;
loading = true;
provider: ProviderResponse;
formPromise: Promise<any>;
taxFormPromise: Promise<any>;
selfHosted = false;
loading = true;
provider: ProviderResponse;
formPromise: Promise<any>;
taxFormPromise: Promise<any>;
private providerId: string;
private providerId: string;
constructor(private apiService: ApiService, private i18nService: I18nService,
private toasterService: ToasterService, private route: ActivatedRoute,
private syncService: SyncService, private platformUtilsService: PlatformUtilsService,
private logService: LogService) { }
constructor(
private apiService: ApiService,
private i18nService: I18nService,
private route: ActivatedRoute,
private syncService: SyncService,
private platformUtilsService: PlatformUtilsService,
private logService: LogService
) {}
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.route.parent.parent.params.subscribe(async params => {
this.providerId = params.providerId;
try {
this.provider = await this.apiService.getProvider(this.providerId);
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
});
this.loading = false;
}
async submit() {
try {
const request = new ProviderUpdateRequest();
request.name = this.provider.name;
request.businessName = this.provider.businessName;
request.billingEmail = this.provider.billingEmail;
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
return this.syncService.fullSync(true);
});
await this.formPromise;
this.toasterService.popAsync('success', null, this.i18nService.t('providerUpdated'));
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
async ngOnInit() {
this.selfHosted = this.platformUtilsService.isSelfHost();
this.route.parent.parent.params.subscribe(async (params) => {
this.providerId = params.providerId;
try {
this.provider = await this.apiService.getProvider(this.providerId);
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
});
this.loading = false;
}
async submit() {
try {
const request = new ProviderUpdateRequest();
request.name = this.provider.name;
request.businessName = this.provider.businessName;
request.billingEmail = this.provider.billingEmail;
this.formPromise = this.apiService.putProvider(this.providerId, request).then(() => {
return this.syncService.fullSync(true);
});
await this.formPromise;
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerUpdated"));
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
}
}

View File

@@ -1,17 +1,17 @@
<div class="container page-content">
<div class="row">
<div class="col-3">
<div class="card">
<div class="card-header">{{'settings' | i18n}}</div>
<div class="list-group list-group-flush">
<a routerLink="account" class="list-group-item" routerLinkActive="active">
{{'myProvider' | i18n}}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
<div class="row">
<div class="col-3">
<div class="card">
<div class="card-header">{{ "settings" | i18n }}</div>
<div class="list-group list-group-flush">
<a routerLink="account" class="list-group-item" routerLinkActive="active">
{{ "myProvider" | i18n }}
</a>
</div>
</div>
</div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

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

View File

@@ -1,27 +1,31 @@
<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>
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-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 && !authed">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'setupProvider' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<p>{{'setupProviderLoginDesc' | i18n}}</p>
<hr>
<div class="d-flex">
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{'logIn' | i18n}}
</a>
</div>
</div>
</div>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "setupProvider" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<p>{{ "setupProviderLoginDesc" | i18n }}</p>
<hr />
<div class="d-flex">
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
{{ "logIn" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,22 +1,21 @@
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({
selector: 'app-setup-provider',
templateUrl: 'setup-provider.component.html',
selector: "app-setup-provider",
templateUrl: "setup-provider.component.html",
})
export class SetupProviderComponent extends BaseAcceptComponent {
failedShortMessage = "inviteAcceptFailedShort";
failedMessage = "inviteAcceptFailed";
failedShortMessage = 'inviteAcceptFailedShort';
failedMessage = 'inviteAcceptFailed';
requiredParameters = ["providerId", "email", "token"];
requiredParameters = ['providerId', 'email', 'token'];
async authedHandler(qParams: any) {
this.router.navigate(["/providers/setup"], { queryParams: qParams });
}
async authedHandler(qParams: any) {
this.router.navigate(['/providers/setup'], {queryParams: qParams});
}
// tslint:disable-next-line
async unauthedHandler(qParams: any) {}
// tslint:disable-next-line
async unauthedHandler(qParams: any) {}
}

View File

@@ -1,32 +1,39 @@
<app-navbar></app-navbar>
<div class="container page-content">
<div class="page-header">
<h1>{{'setupProvider' | i18n}}</h1>
<div class="page-header">
<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>
<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 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 class="mt-4">
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "submit" | i18n }}</span>
</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" *ngIf="showCancel">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
<app-footer></app-footer>

View File

@@ -1,103 +1,99 @@
import {
Component,
OnInit,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import {
Toast,
ToasterService,
} from 'angular2-toaster';
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from 'rxjs/operators';
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 { CryptoService } from "jslib-common/abstractions/crypto.service";
import { ValidationService } from 'jslib-angular/services/validation.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { SyncService } from 'jslib-common/abstractions/sync.service';
import { ProviderSetupRequest } from 'jslib-common/models/request/provider/providerSetupRequest';
import { ValidationService } from "jslib-angular/services/validation.service";
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({
selector: 'provider-setup',
templateUrl: 'setup.component.html',
selector: "provider-setup",
templateUrl: "setup.component.html",
})
export class SetupComponent implements OnInit {
loading = true;
authed = false;
email: string;
formPromise: Promise<any>;
loading = true;
authed = false;
email: string;
formPromise: Promise<any>;
providerId: string;
token: string;
name: string;
billingEmail: string;
providerId: string;
token: string;
name: string;
billingEmail: string;
constructor(private router: Router, private toasterService: ToasterService,
private i18nService: I18nService, private route: ActivatedRoute,
private cryptoService: CryptoService, private apiService: ApiService,
private syncService: SyncService, private validationService: ValidationService) { }
constructor(
private router: Router,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private route: ActivatedRoute,
private cryptoService: CryptoService,
private apiService: ApiService,
private syncService: SyncService,
private validationService: ValidationService
) {}
ngOnInit() {
document.body.classList.remove('layout_frontend');
this.route.queryParams.pipe(first()).subscribe(async qParams => {
const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
ngOnInit() {
document.body.classList.remove("layout_frontend");
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
if (error) {
const toast: Toast = {
type: 'error',
title: null,
body: this.i18nService.t('emergencyInviteAcceptFailed'),
timeout: 10000,
};
this.toasterService.popAsync(toast);
this.router.navigate(['/']);
return;
}
if (error) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("emergencyInviteAcceptFailed"),
{ timeout: 10000 }
);
this.router.navigate(["/"]);
return;
}
this.providerId = qParams.providerId;
this.token = qParams.token;
this.providerId = qParams.providerId;
this.token = qParams.token;
// Check if provider exists, redirect if it does
try {
const provider = await this.apiService.getProvider(this.providerId);
if (provider.name != null) {
this.router.navigate(['/providers', provider.id], { replaceUrl: true });
}
} catch (e) {
this.validationService.showError(e);
this.router.navigate(['/']);
}
});
}
async submit() {
this.formPromise = this.doSubmit();
await this.formPromise;
this.formPromise = null;
}
async doSubmit() {
try {
const shareKey = await this.cryptoService.makeShareKey();
const key = shareKey[0].encryptedString;
const request = new ProviderSetupRequest();
request.name = this.name;
request.billingEmail = this.billingEmail;
request.token = this.token;
request.key = key;
const provider = await this.apiService.postProviderSetup(this.providerId, request);
this.toasterService.popAsync('success', null, this.i18nService.t('providerSetup'));
await this.syncService.fullSync(true);
this.router.navigate(['/providers', provider.id]);
} catch (e) {
this.validationService.showError(e);
// Check if provider exists, redirect if it does
try {
const provider = await this.apiService.getProvider(this.providerId);
if (provider.name != null) {
this.router.navigate(["/providers", provider.id], { replaceUrl: true });
}
} catch (e) {
this.validationService.showError(e);
this.router.navigate(["/"]);
}
});
}
async submit() {
this.formPromise = this.doSubmit();
await this.formPromise;
this.formPromise = null;
}
async doSubmit() {
try {
const shareKey = await this.cryptoService.makeShareKey();
const key = shareKey[0].encryptedString;
const request = new ProviderSetupRequest();
request.name = this.name;
request.billingEmail = this.billingEmail;
request.token = this.token;
request.key = key;
const provider = await this.apiService.postProviderSetup(this.providerId, request);
this.platformUtilsService.showToast("success", null, this.i18nService.t("providerSetup"));
await this.syncService.fullSync(true);
this.router.navigate(["/providers", provider.id]);
} catch (e) {
this.validationService.showError(e);
}
}
}

View File

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

View File

@@ -1,37 +1,36 @@
function load(envName) {
return {
...require('./config/base.json'),
...loadConfig(envName),
...loadConfig('local'),
dev: {
...require('./config/base.json').dev,
...loadConfig(envName).dev,
...loadConfig('local').dev,
},
};
return {
...require("./config/base.json"),
...loadConfig(envName),
...loadConfig("local"),
dev: {
...require("./config/base.json").dev,
...loadConfig(envName).dev,
...loadConfig("local").dev,
},
};
}
function log(configObj) {
const repeatNum = 50;
console.log(`${"=".repeat(repeatNum)}\nenvConfig`);
console.log(JSON.stringify(configObj, null, 2));
console.log(`${"=".repeat(repeatNum)}`);
const repeatNum = 50;
console.log(`${"=".repeat(repeatNum)}\nenvConfig`);
console.log(JSON.stringify(configObj, null, 2));
console.log(`${"=".repeat(repeatNum)}`);
}
function loadConfig(configName) {
try {
return require(`./config/${configName}.json`);
} catch (e) {
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
return {};
}
else {
throw e;
}
try {
return require(`./config/${configName}.json`);
} catch (e) {
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
return {};
} else {
throw e;
}
}
}
module.exports = {
load,
log
load,
log,
};

View File

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

View File

@@ -1,17 +1,17 @@
{
"urls": {
"icons": "https://icons.bitwarden.net",
"notifications": "https://notifications.bitwarden.com"
},
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
"paypal": {
"businessId": "4ZDA7DLUUJGMN",
"buttonAction": "https://www.paypal.com/cgi-bin/webscr"
},
"dev": {
"proxyApi": "https://api.bitwarden.com",
"proxyIdentity": "https://identity.bitwarden.com",
"proxyEvents": "https://events.bitwarden.com"
}
"urls": {
"icons": "https://icons.bitwarden.net",
"notifications": "https://notifications.bitwarden.com"
},
"stripeKey": "pk_live_bpN0P37nMxrMQkcaHXtAybJk",
"braintreeKey": "production_qfbsv8kc_njj2zjtyngtjmbjd",
"paypal": {
"businessId": "4ZDA7DLUUJGMN",
"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,11 +1,11 @@
{
"urls": {
"notifications": "http://localhost:61840"
},
"dev": {
"proxyApi": "http://localhost:4000",
"proxyIdentity": "http://localhost:33656",
"proxyEvents": "http://localhost:46273",
"proxyNotifications": "http://localhost:61840"
}
"urls": {
"notifications": "http://localhost:61840"
},
"dev": {
"proxyApi": "http://localhost:4000",
"proxyIdentity": "http://localhost:33656",
"proxyEvents": "http://localhost:46273",
"proxyNotifications": "http://localhost:61840"
}
}

View File

@@ -1,11 +1,11 @@
{
"urls": {
"icons": "https://icons.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"
}
"urls": {
"icons": "https://icons.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

@@ -32,7 +32,6 @@ mkhomedir_helper $USERNAME
chown -R $USERNAME:$GROUPNAME /etc/bitwarden
cp /etc/bitwarden/web/app-id.json /app/app-id.json
chown -R $USERNAME:$GROUPNAME /app
chown -R $USERNAME:$GROUPNAME /bitwarden_server
#chown -R $USERNAME:$GROUPNAME /bitwarden_server
exec gosu $USERNAME:$GROUPNAME dotnet /bitwarden_server/Server.dll \
/contentRoot=/app /webRoot=. /serveUnknown=false /webVault=true
#exec nginx -g 'daemon off;'

138
docker/mime.types Normal file
View File

@@ -0,0 +1,138 @@
types {
# Data interchange
application/atom+xml atom;
application/json json map topojson;
application/ld+json jsonld;
application/rss+xml rss;
application/vnd.geo+json geojson;
application/xml rdf xml;
# JavaScript
# Normalize to standard type.
# https://tools.ietf.org/html/rfc4329#section-7.2
application/javascript js;
# Manifest files
application/manifest+json webmanifest;
application/x-web-app-manifest+json webapp;
text/cache-manifest appcache;
# Media files
audio/midi mid midi kar;
audio/mp4 aac f4a f4b m4a;
audio/mpeg mp3;
audio/ogg oga ogg opus;
audio/x-realaudio ra;
audio/x-wav wav;
image/bmp bmp;
image/gif gif;
image/jpeg jpeg jpg;
image/jxr jxr hdp wdp;
image/png png;
image/svg+xml svg svgz;
image/tiff tif tiff;
image/vnd.wap.wbmp wbmp;
image/webp webp;
image/x-jng jng;
video/3gpp 3gp 3gpp;
video/mp4 f4p f4v m4v mp4;
video/mpeg mpeg mpg;
video/ogg ogv;
video/quicktime mov;
video/webm webm;
video/x-flv flv;
video/x-mng mng;
video/x-ms-asf asf asx;
video/x-ms-wmv wmv;
video/x-msvideo avi;
# Serving `.ico` image files with a different media type
# prevents Internet Explorer from displaying then as images:
# https://github.com/h5bp/html5-boilerplate/commit/37b5fec090d00f38de64b591bcddcb205aadf8ee
image/x-icon cur ico;
# Microsoft Office
application/msword doc;
application/vnd.ms-excel xls;
application/vnd.ms-powerpoint ppt;
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx;
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx;
# Web fonts
application/font-woff woff;
application/font-woff2 woff2;
application/vnd.ms-fontobject eot;
# Browsers usually ignore the font media types and simply sniff
# the bytes to figure out the font type.
# https://mimesniff.spec.whatwg.org/#matching-a-font-type-pattern
#
# However, Blink and WebKit based browsers will show a warning
# in the console if the following font types are served with any
# other media types.
application/x-font-ttf ttc ttf;
font/opentype otf;
# Other
application/java-archive ear jar war;
application/mac-binhex40 hqx;
application/octet-stream bin deb dll dmg exe img iso msi msm msp safariextz;
application/pdf pdf;
application/postscript ai eps ps;
application/rtf rtf;
application/vnd.google-earth.kml+xml kml;
application/vnd.google-earth.kmz kmz;
application/vnd.wap.wmlc wmlc;
application/x-7z-compressed 7z;
application/x-bb-appworld bbaw;
application/x-bittorrent torrent;
application/x-chrome-extension crx;
application/x-cocoa cco;
application/x-java-archive-diff jardiff;
application/x-java-jnlp-file jnlp;
application/x-makeself run;
application/x-opera-extension oex;
application/x-perl pl pm;
application/x-pilot pdb prc;
application/x-rar-compressed rar;
application/x-redhat-package-manager rpm;
application/x-sea sea;
application/x-shockwave-flash swf;
application/x-stuffit sit;
application/x-tcl tcl tk;
application/x-x509-ca-cert crt der pem;
application/x-xpinstall xpi;
application/xhtml+xml xhtml;
application/xslt+xml xsl;
application/zip zip;
text/css css;
text/csv csv;
text/html htm html shtml;
text/markdown md;
text/mathml mml;
text/plain txt;
text/vcard vcard vcf;
text/vnd.rim.location.xloc xloc;
text/vnd.sun.j2me.app-descriptor jad;
text/vnd.wap.wml wml;
text/vtt vtt;
text/x-component htc;
}

25
docker/nginx-web.conf Normal file
View File

@@ -0,0 +1,25 @@
#######################################################################
# WARNING: This file is generated. Do not make changes to this file. #
# They will be overwritten on update. You can manage various settings #
# used in this file from the ./bwdata/config.yml file for your #
# installation. #
#######################################################################
server {
listen 8080 default_server;
listen [::]:8080 default_server;
include /etc/nginx/security-headers.conf;
location / {
root /app;
index index.html index.htm;
include /etc/nginx/security-headers.conf;
add_header X-Frame-Options SAMEORIGIN;
add_header X-Robots-Tag "noindex, nofollow";
}
location /alive {
return 200 'alive';
add_header Content-Type text/plain;
}
}

147
docker/nginx.conf Normal file
View File

@@ -0,0 +1,147 @@
# nginx Configuration File
# http://wiki.nginx.org/Configuration
# Run as a less privileged user for security reasons.
# user www www;
# How many worker threads to run;
# "auto" sets it to the number of CPU cores available in the system, and
# offers the best performance. Don't set it higher than the number of CPU
# cores if changing this parameter.
# The maximum number of connections for Nginx is calculated by:
# max_clients = worker_processes * worker_connections
worker_processes auto;
# Maximum open file descriptors per process;
# should be > worker_connections.
worker_rlimit_nofile 8192;
events {
# When you need > 8000 * cpu_cores connections, you start optimizing your OS,
# and this is probably the point at which you hire people who are smarter than
# you, as this is *a lot* of requests.
worker_connections 8000;
}
# Default error log file
# (this is only used when you don't override error_log on a server{} level)
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
http {
# Hide nginx version information.
server_tokens off;
# Define the MIME types for files.
include mime.types;
default_type application/octet-stream;
# Update charset_types to match updated mime.types.
# text/html is always included by charset module.
# Default: text/html text/xml text/plain text/vnd.wap.wml application/javascript application/rss+xml
charset_types
text/css
text/plain
text/vnd.wap.wml
application/javascript
application/json
application/rss+xml
application/xml;
# Format to use in log files
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# Default log file
# (this is only used when you don't override access_log on a server{} level)
access_log /var/log/nginx/access.log main;
# How long to allow each connection to stay idle; longer values are better
# for each individual client, particularly for SSL, but means that worker
# connections are tied up longer. (Default: 65)
keepalive_timeout 20;
# Speed up file transfers by using sendfile() to copy directly
# between descriptors rather than using read()/write().
# For performance reasons, on FreeBSD systems w/ ZFS
# this option should be disabled as ZFS's ARC caches
# frequently used files in RAM by default.
sendfile on;
# Tell Nginx not to send out partial frames; this increases throughput
# since TCP frames are filled up before being sent out. (adds TCP_CORK)
tcp_nopush on;
# Compression
# Enable Gzip compressed.
gzip on;
# Compression level (1-9).
# 5 is a perfect compromise between size and cpu usage, offering about
# 75% reduction for most ascii files (almost identical to level 9).
gzip_comp_level 5;
# Don't compress anything that's already small and unlikely to shrink much
# if at all (the default is 20 bytes, which is bad as that usually leads to
# larger files after gzipping).
gzip_min_length 256;
# Compress data even for clients that are connecting to us via proxies,
# identified by the "Via" header (required for CloudFront).
gzip_proxied any;
# Tell proxies to cache both the gzipped and regular version of a resource
# whenever the client's Accept-Encoding capabilities header varies;
# Avoids the issue where a non-gzip capable client (which is extremely rare
# today) would display gibberish if their proxy gave them the gzipped version.
gzip_vary on;
# Compress all output labeled with one of the following MIME-types.
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
# text/html is always compressed by HttpGzipModule
# This should be turned on if you are going to have pre-compressed copies (.gz) of
# static files available. If not it should be left off as it will cause extra I/O
# for the check. It is best if you enable this in a location{} block for
# a specific directory, or on an individual server{} level.
# gzip_static on;
# Content type for FIDO U2F facets
map $uri $fido_content_type {
default "application/fido.trusted-apps+json";
}
# Include files in the sites-enabled folder. server{} configuration files should be
# placed in the sites-available folder, and then the configuration should be enabled
# by creating a symlink to it in the sites-enabled folder.
# See doc/sites-enabled.md for more info.
include conf.d/*.conf;
}

View File

@@ -0,0 +1,3 @@
add_header Referrer-Policy same-origin;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

2
jslib

Submodule jslib updated: b4f475251a...e372bf242b

13788
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "bitwarden-web",
"version": "2.24.4",
"name": "@bitwarden/web-vault",
"version": "2.25.1",
"license": "GPL-3.0",
"repository": "https://github.com/bitwarden/web",
"scripts": {
@@ -29,44 +29,57 @@
"dist:bit:selfhost": "npm run build:bit:selfhost:prod",
"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",
"lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' || true",
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix"
"lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' && prettier --check .",
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix",
"prettier": "prettier --write .",
"prepare": "husky install"
},
"devDependencies": {
"@angular/compiler-cli": "^11.2.11",
"@ngtools/webpack": "^11.2.10",
"@angular/compiler-cli": "^12.2.13",
"@ngtools/webpack": "^12.2.13",
"@types/jquery": "^3.5.5",
"@types/node": "^14.17.2",
"@types/node": "^16.11.12",
"@types/webcrypto": "^0.0.28",
"@types/webpack": "^4.4.27",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.4.0",
"@types/webpack": "^5.28.0",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^10.0.0",
"cross-env": "^7.0.3",
"css-loader": "^5.2.3",
"del": "^6.0.0",
"file-loader": "^6.2.0",
"css-loader": "^6.5.1",
"gh-pages": "^3.1.0",
"html-loader": "^1.3.2",
"html-loader": "^3.0.1",
"html-webpack-injector": "1.1.4",
"html-webpack-plugin": "^4.5.1",
"mini-css-extract-plugin": "^1.5.0",
"html-webpack-plugin": "^5.5.0",
"husky": "^7.0.4",
"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-loader": "^10.1.1",
"style-loader": "^2.0.0",
"tapable": "^1.1.3",
"terser-webpack-plugin": "^4.2.3",
"ts-loader": "^8.1.0",
"sass-loader": "^12.4.0",
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.2.5",
"ts-loader": "^9.2.5",
"tslint": "^6.1.3",
"tslint-loader": "^3.5.4",
"typescript": "4.1.5",
"webpack": "^4.46.0",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^3.11.2"
"typescript": "4.3.5",
"util": "^0.12.4",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.6.0"
},
"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-common": "file:jslib/common",
"angular2-toaster": "11.0.1",
"bootstrap": "4.6.0",
"braintree-web-drop-in": "1.30.1",
"browser-hrtime": "^1.1.8",
@@ -75,14 +88,20 @@
"font-awesome": "4.7.0",
"jquery": "3.6.0",
"ngx-infinite-scroll": "^10.0.1",
"ngx-toastr": "14.1.4",
"popper.js": "1.16.1",
"qrious": "4.0.2",
"rxjs": "^7.4.0",
"sweetalert2": "^10.16.6",
"webcrypto-shim": "0.1.7",
"whatwg-fetch": "3.6.2"
},
"engines": {
"node": "~14",
"npm": "~7"
"node": "~16",
"npm": "~8"
},
"lint-staged": {
"*": "prettier --ignore-unknown --write",
"*.png": "node scripts/optimize.js"
}
}

21
scripts/optimize.js Normal file
View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,34 +1,41 @@
<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>
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-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 && !authed">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'emergencyAccess' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<p class="text-center">
{{name}}
</p>
<p>{{'acceptEmergencyAccess' | i18n}}</p>
<hr>
<div class="d-flex">
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{'logIn' | i18n}}
</a>
<a routerLink="/register" [queryParams]="{email: email}"
class="btn btn-primary btn-block ml-2 mt-0">
{{'createAccount' | i18n}}
</a>
</div>
</div>
</div>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "emergencyAccess" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<p class="text-center">
{{ name }}
</p>
<p>{{ "acceptEmergencyAccess" | i18n }}</p>
<hr />
<div class="d-flex">
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
{{ "logIn" | i18n }}
</a>
<a
routerLink="/register"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block ml-2 mt-0"
>
{{ "createAccount" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,60 +1,54 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import {
Toast,
ToasterService,
} from 'angular2-toaster';
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';
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 { StateService } from "jslib-common/abstractions/state.service";
import { EmergencyAccessAcceptRequest } from "jslib-common/models/request/emergencyAccessAcceptRequest";
import { BaseAcceptComponent } from "../common/base.accept.component";
@Component({
selector: 'app-accept-emergency',
templateUrl: 'accept-emergency.component.html',
selector: "app-accept-emergency",
templateUrl: "accept-emergency.component.html",
})
export class AcceptEmergencyComponent extends BaseAcceptComponent {
name: string;
name: string;
protected requiredParameters: string[] = ["id", "name", "email", "token"];
protected failedShortMessage = "emergencyInviteAcceptFailedShort";
protected failedMessage = "emergencyInviteAcceptFailed";
protected requiredParameters: string[] = ['id', 'name', 'email', 'token'];
protected failedShortMessage = 'emergencyInviteAcceptFailedShort';
protected failedMessage = 'emergencyInviteAcceptFailed';
constructor(
router: Router,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
route: ActivatedRoute,
private apiService: ApiService,
stateService: StateService
) {
super(router, platformUtilsService, i18nService, route, stateService);
}
constructor(router: Router, toasterService: ToasterService,
i18nService: I18nService, route: ActivatedRoute,
private apiService: ApiService, userService: UserService,
stateService: StateService) {
super(router, toasterService, i18nService, route, userService, stateService);
}
async authedHandler(qParams: any): Promise<void> {
const request = new EmergencyAccessAcceptRequest();
request.token = qParams.token;
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
await this.actionPromise;
const toast: Toast = {
type: 'success',
title: this.i18nService.t('inviteAccepted'),
body: this.i18nService.t('emergencyInviteAcceptedDesc'),
timeout: 10000,
};
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, ' ');
}
async authedHandler(qParams: any): Promise<void> {
const request = new EmergencyAccessAcceptRequest();
request.token = qParams.token;
this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
await this.actionPromise;
this.platformUtilService.showToast(
"success",
this.i18nService.t("inviteAccepted"),
this.i18nService.t("emergencyInviteAcceptedDesc"),
{ timeout: 10000 }
);
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,35 +1,42 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div>
<img src="../../images/logo-dark@2x.png" class="mb-4 logo" 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>
<img src="../../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden" />
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-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 && !authed">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'joinOrganization' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<p class="text-center">
{{orgName}}
<strong class="d-block mt-2">{{email}}</strong>
</p>
<p>{{'joinOrganizationDesc' | i18n}}</p>
<hr>
<div class="d-flex">
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block">
{{'logIn' | i18n}}
</a>
<a routerLink="/register" [queryParams]="{email: email}"
class="btn btn-primary btn-block ml-2 mt-0">
{{'createAccount' | i18n}}
</a>
</div>
</div>
</div>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "joinOrganization" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<p class="text-center">
{{ orgName }}
<strong class="d-block mt-2">{{ email }}</strong>
</p>
<p>{{ "joinOrganizationDesc" | i18n }}</p>
<hr />
<div class="d-flex">
<a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
{{ "logIn" | i18n }}
</a>
<a
routerLink="/register"
[queryParams]="{ email: email }"
class="btn btn-primary btn-block ml-2 mt-0"
>
{{ "createAccount" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,117 +1,127 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import {
Toast,
ToasterService,
} from 'angular2-toaster';
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 { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { CryptoService } from 'jslib-common/abstractions/crypto.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { PolicyService } from 'jslib-common/abstractions/policy.service';
import { StateService } from 'jslib-common/abstractions/state.service';
import { UserService } from 'jslib-common/abstractions/user.service';
import { OrganizationUserAcceptRequest } from "jslib-common/models/request/organizationUserAcceptRequest";
import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
import { OrganizationUserAcceptRequest } from 'jslib-common/models/request/organizationUserAcceptRequest';
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest';
import { Utils } from 'jslib-common/misc/utils';
import { Policy } from 'jslib-common/models/domain/policy';
import { BaseAcceptComponent } from '../common/base.accept.component';
import { Utils } from "jslib-common/misc/utils";
import { Policy } from "jslib-common/models/domain/policy";
import { BaseAcceptComponent } from "../common/base.accept.component";
@Component({
selector: 'app-accept-organization',
templateUrl: 'accept-organization.component.html',
selector: "app-accept-organization",
templateUrl: "accept-organization.component.html",
})
export class AcceptOrganizationComponent extends BaseAcceptComponent {
orgName: string;
orgName: string;
protected requiredParameters: string[] = ['organizationId', 'organizationUserId', 'token'];
protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"];
constructor(router: Router, toasterService: ToasterService,
i18nService: I18nService, route: ActivatedRoute,
private apiService: ApiService, userService: UserService,
stateService: StateService, private cryptoService: CryptoService,
private policyService: PolicyService, private logService: LogService) {
super(router, toasterService, i18nService, route, userService, stateService);
constructor(
router: Router,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
route: ActivatedRoute,
private apiService: ApiService,
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
);
}
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'));
}
await this.actionPromise;
this.platformUtilService.showToast(
"success",
this.i18nService.t("inviteAccepted"),
this.i18nService.t("inviteAcceptedDesc"),
{ timeout: 10000 }
);
const publicKey = Utils.fromB64ToArray(response.publicKey);
await this.stateService.setOrganizationInvitation(null);
this.router.navigate(["/vault"]);
}
// 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);
async unauthedHandler(qParams: any): Promise<void> {
this.orgName = qParams.organizationName;
if (this.orgName != null) {
// Fix URL encoding of space issue with Angular
this.orgName = this.orgName.replace(/\+/g, " ");
}
await this.stateService.setOrganizationInvitation(qParams);
}
// Create request and execute enrollment
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest();
resetRequest.resetPasswordKey = encryptedKey.encryptedString;
// Get User Id
const userId = await this.userService.getUserId();
return this.apiService.putOrganizationUserResetPasswordEnrollment(qParams.organizationId, userId, resetRequest);
});
} else {
this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId,
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']);
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 (e) {
this.logService.error(e);
}
async unauthedHandler(qParams: any): Promise<void> {
this.orgName = qParams.organizationName;
if (this.orgName != null) {
// Fix URL encoding of space issue with Angular
this.orgName = this.orgName.replace(/\+/g, ' ');
}
await this.stateService.save('orgInvitation', qParams);
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;
}
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 (e) {
this.logService.error(e);
}
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;
}
return false;
}
}

View File

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

View File

@@ -1,21 +1,25 @@
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { Component } from "@angular/core";
import { Router } from "@angular/router";
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 { 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 { HintComponent as BaseHintComponent } from 'jslib-angular/components/hint.component';
import { HintComponent as BaseHintComponent } from "jslib-angular/components/hint.component";
@Component({
selector: 'app-hint',
templateUrl: 'hint.component.html',
selector: "app-hint",
templateUrl: "hint.component.html",
})
export class HintComponent extends BaseHintComponent {
constructor(router: Router, i18nService: I18nService,
apiService: ApiService, platformUtilsService: PlatformUtilsService,
logService: LogService) {
super(router, i18nService, apiService, platformUtilsService, logService);
}
constructor(
router: Router,
i18nService: I18nService,
apiService: ApiService,
platformUtilsService: PlatformUtilsService,
logService: LogService
) {
super(router, i18nService, apiService, platformUtilsService, logService);
}
}

View File

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

View File

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

View File

@@ -1,61 +1,102 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<img class="mb-2 logo logo-themed" alt="Bitwarden">
<p class="lead text-center mx-4 mb-4">{{'loginOrCreateNewAccount' | i18n}}</p>
<div class="card d-block">
<div class="card-body">
<app-callout type="warning" title="{{'resetPasswordPolicyAutoEnroll' | i18n}}"
*ngIf="showResetPasswordAutoEnrollWarning">
{{'resetPasswordAutoEnrollInviteWarning' | 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
inputmode="email" appInputVerbatim="false">
</div>
<div class="form-group">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<div class="d-flex">
<input id="masterPassword" type="{{showPassword ? 'text' : 'password'}}"
name="MasterPassword" class="text-monospace form-control" [(ngModel)]="masterPassword"
required appInputVerbatim>
<button type="button" class="ml-1 btn btn-link" appA11yTitle="{{'toggleVisibility' | i18n}}"
(click)="togglePassword()">
<i class="fa fa-lg" aria-hidden="true"
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
</button>
</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 class="row justify-content-md-center mt-5">
<div class="col-5">
<img class="mb-2 logo logo-themed" alt="Bitwarden" />
<p class="lead text-center mx-4 mb-4">{{ "loginOrCreateNewAccount" | i18n }}</p>
<div class="card d-block">
<div class="card-body">
<app-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="showResetPasswordAutoEnrollWarning"
>
{{ "resetPasswordAutoEnrollInviteWarning" | 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
inputmode="email"
appInputVerbatim="false"
/>
</div>
<div class="form-group">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<div class="d-flex">
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="text-monospace form-control"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</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="bwi bwi-sign-in" aria-hidden="true"></i> {{ "logIn" | i18n }} </span>
<i
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a
routerLink="/register"
[queryParams]="{ email: email }"
class="btn btn-outline-secondary btn-block ml-2 mt-0"
>
<i class="bwi bwi-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="bwi bwi-bank" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@@ -1,98 +1,118 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { Component, NgZone } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from 'rxjs/operators';
import { first } from "rxjs/operators";
import { ApiService } from 'jslib-common/abstractions/api.service';
import { AuthService } from 'jslib-common/abstractions/auth.service';
import { CryptoFunctionService } from 'jslib-common/abstractions/cryptoFunction.service';
import { EnvironmentService } from 'jslib-common/abstractions/environment.service';
import { I18nService } from 'jslib-common/abstractions/i18n.service';
import { LogService } from 'jslib-common/abstractions/log.service';
import { 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 { 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 { 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({
selector: 'app-login',
templateUrl: 'login.component.html',
selector: "app-login",
templateUrl: "login.component.html",
})
export class LoginComponent extends BaseLoginComponent {
showResetPasswordAutoEnrollWarning = false;
showResetPasswordAutoEnrollWarning = false;
constructor(
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;
}
constructor(authService: AuthService, router: Router,
i18nService: I18nService, private route: ActivatedRoute,
storageService: StorageService, stateService: StateService,
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService,
private apiService: ApiService, private policyService: PolicyService, logService: LogService) {
super(authService, router,
platformUtilsService, i18nService,
stateService, environmentService,
passwordGenerationService, cryptoFunctionService,
storageService, logService);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}
async ngOnInit() {
this.route.queryParams.pipe(first()).subscribe(async qParams => {
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.email = qParams.email;
}
if (qParams.premium != null) {
this.stateService.save('loginRedirect', { route: '/settings/premium' });
} else if (qParams.org != null) {
this.stateService.save('loginRedirect',
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
}
// 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.save('loginRedirect', {
route: '/setup/families-for-enterprise',
qParams: { token: qParams.sponsorshipToken },
});
}
await super.ngOnInit();
async ngOnInit() {
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
this.email = qParams.email;
}
if (qParams.premium != null) {
this.stateService.setLoginRedirect({ route: "/settings/premium" });
} else if (qParams.org != null) {
this.stateService.setLoginRedirect({
route: "/settings/create-organization",
qParams: { plan: qParams.org },
});
}
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 (e) {
this.logService.error(e);
}
// 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();
});
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;
}
}
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() {
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]);
}
async goAfterLogIn() {
const loginRedirect = await this.stateService.getLoginRedirect();
if (loginRedirect != null) {
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
await this.stateService.setLoginRedirect(null);
} else {
this.router.navigate([this.successRoute]);
}
}
}

View File

@@ -1,27 +1,44 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'deleteAccount' | i18n}}</p>
<div class="card">
<div class="card-body">
<p>{{'deleteRecoverDesc' | i18n}}</p>
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
appAutofocus inputmode="email" appInputVerbatim="false">
</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 class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "deleteAccount" | i18n }}</p>
<div class="card">
<div class="card-body">
<p>{{ "deleteRecoverDesc" | i18n }}</p>
<div class="form-group">
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
appAutofocus
inputmode="email"
appInputVerbatim="false"
/>
</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="bwi bwi-spinner bwi-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>
</form>

View File

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

View File

@@ -1,40 +1,76 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'recoverAccountTwoStep' | i18n}}</p>
<div class="card">
<div class="card-body">
<p>{{'recoverAccountTwoStepDesc' | i18n}}
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank"
rel="noopener">{{'learnMore' | i18n}}</a>
</p>
<div class="form-group">
<label for="email">{{'emailAddress' | i18n}}</label>
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required
appAutofocus inputmode="email" appInputVerbatim="false">
</div>
<div class="form-group">
<label for="masterPassword">{{'masterPass' | i18n}}</label>
<input id="masterPassword" type="password" name="MasterPassword" class="form-control"
[(ngModel)]="masterPassword" 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 class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "recoverAccountTwoStep" | i18n }}</p>
<div class="card">
<div class="card-body">
<p>
{{ "recoverAccountTwoStepDesc" | i18n }}
<a
href="https://help.bitwarden.com/article/lost-two-step-device/"
target="_blank"
rel="noopener"
>{{ "learnMore" | i18n }}</a
>
</p>
<div class="form-group">
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
appAutofocus
inputmode="email"
appInputVerbatim="false"
/>
</div>
<div class="form-group">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="password"
name="MasterPassword"
class="form-control"
[(ngModel)]="masterPassword"
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="bwi bwi-spinner bwi-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>
</form>

View File

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

View File

@@ -1,141 +1,216 @@
<div class="layout" [ngClass]="['layout', layout]">
<header class="header" *ngIf="layout === 'enterprise2'">
<div class="container">
<div class="row">
<div class="col-7">
<img alt="Bitwarden" class="logo mb-2" src="../../images/register-layout/logo-horizontal-white.png">
</div>
</div>
<header class="header" *ngIf="layout === 'enterprise2'">
<div class="container">
<div class="row">
<div class="col-7">
<img
alt="Bitwarden"
class="logo mb-2"
src="../../images/register-layout/logo-horizontal-white.png"
/>
</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 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>
<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>
</form>
</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="bwi bwi-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="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{
'bwi-eye': !showPassword,
'bwi-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="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-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="bwi bwi-spinner bwi-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>

View File

@@ -1,115 +1,150 @@
import { Component } from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from 'rxjs/operators';
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 { 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 { 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 { 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 { 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 { Policy } from "jslib-common/models/domain/policy";
import { PolicyData } from 'jslib-common/models/data/policyData';
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({
selector: 'app-register',
templateUrl: 'register.component.html',
selector: "app-register",
templateUrl: "register.component.html",
})
export class RegisterComponent extends BaseRegisterComponent {
showCreateOrgMessage = false;
layout = '';
enforcedPolicyOptions: MasterPasswordPolicyOptions;
showCreateOrgMessage = false;
layout = "";
enforcedPolicyOptions: MasterPasswordPolicyOptions;
private policies: Policy[];
private policies: Policy[];
constructor(authService: AuthService, router: Router,
i18nService: I18nService, cryptoService: CryptoService,
apiService: ApiService, private route: ActivatedRoute,
stateService: StateService, platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService, private policyService: PolicyService,
environmentService: EnvironmentService, logService: LogService) {
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
passwordGenerationService, environmentService, logService);
}
constructor(
authService: AuthService,
router: Router,
i18nService: I18nService,
cryptoService: CryptoService,
apiService: ApiService,
private route: ActivatedRoute,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService,
private policyService: PolicyService,
environmentService: EnvironmentService,
logService: LogService
) {
super(
authService,
router,
i18nService,
cryptoService,
apiService,
stateService,
platformUtilsService,
passwordGenerationService,
environmentService,
logService
);
}
async ngOnInit() {
this.route.queryParams.pipe(first()).subscribe(qParams => {
this.referenceData = new ReferenceEventRequest();
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
this.email = qParams.email;
}
if (qParams.premium != null) {
this.stateService.save('loginRedirect', { route: '/settings/premium' });
} else if (qParams.org != null) {
this.showCreateOrgMessage = true;
this.referenceData.flow = qParams.org;
this.stateService.save('loginRedirect',
{ route: '/settings/create-organization', 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();
}
// 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.save('loginRedirect', {
route: '/setup/families-for-enterprise',
qParams: { token: qParams.sponsorshipToken },
});
}
if (this.referenceData.id === '') {
this.referenceData.id = null;
}
async ngOnInit() {
this.route.queryParams.pipe(first()).subscribe((qParams) => {
this.referenceData = new ReferenceEventRequest();
if (qParams.email != null && qParams.email.indexOf("@") > -1) {
this.email = qParams.email;
}
if (qParams.premium != null) {
this.stateService.setLoginRedirect({ route: "/settings/premium" });
} else if (qParams.org != null) {
this.showCreateOrgMessage = true;
this.referenceData.flow = qParams.org;
this.stateService.setLoginRedirect({
route: "/settings/create-organization",
qParams: { plan: qParams.org },
});
const invite = await this.stateService.get<any>('orgInvitation');
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 (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();
}
// 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));
}
if (this.policies != null) {
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(this.policies);
}
await super.ngOnInit();
} catch (e) {
this.logService.error(e);
}
}
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();
if (this.policies != null) {
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions(
this.policies
);
}
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,31 +1,55 @@
<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>
<img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-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>
<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>
<button
type="button"
class="btn btn-primary btn-block"
(click)="convert()"
[disabled]="actionPromise"
>
<i
class="bwi bwi-spinner bwi-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="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="leaving"
></i>
{{ "leaveOrganization" | i18n }}
</button>
</div>
</div>
</div>
</div>
</div>

View File

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

View File

@@ -1,73 +1,117 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{'setMasterPassword' | i18n}}</p>
<div class="card d-block">
<div class="card-body text-center" *ngIf="syncLoading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
{{'loading' | i18n}}
</div>
<div class="card-body" *ngIf="!syncLoading">
<app-callout type="info">{{'ssoCompleteRegistration' | i18n}}</app-callout>
<app-callout type="warning" title="{{'resetPasswordPolicyAutoEnroll' | i18n}}"
*ngIf="resetPasswordAutoEnroll">
{{'resetPasswordAutoEnrollInviteWarning' | i18n}}
</app-callout>
<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="MasterPasswordHash" 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)]="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 class="row justify-content-md-center mt-5">
<div class="col-5">
<p class="lead text-center mb-4">{{ "setMasterPassword" | i18n }}</p>
<div class="card d-block">
<div class="card-body text-center" *ngIf="syncLoading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<div class="card-body" *ngIf="!syncLoading">
<app-callout type="info">{{ "ssoCompleteRegistration" | i18n }}</app-callout>
<app-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="resetPasswordAutoEnroll"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</app-callout>
<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="MasterPasswordHash"
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="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-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)]="masterPasswordRetype"
required
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-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="bwi bwi-spinner bwi-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>
</form>

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