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

Apply Prettier (#1347)

This commit is contained in:
Oscar Hinton
2021-12-17 15:57:11 +01:00
committed by GitHub
parent 2b0a9d995e
commit 56477eb39c
414 changed files with 33390 additions and 26857 deletions

4
.gitattributes vendored
View File

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

View File

@@ -6,7 +6,7 @@ body:
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this bug report! Thanks for taking the time to fill out this bug report!
Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests. Please do not submit feature requests. The [Community Forums](https://community.bitwarden.com) has a section for submitting, voting for, and discussing product feature requests.
- type: textarea - type: textarea
id: reproduce id: reproduce

View File

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

View File

@@ -9,8 +9,8 @@ on:
required: false required: false
push: push:
branches-ignore: branches-ignore:
- 'l10n_master' - "l10n_master"
- 'gh-pages' - "gh-pages"
jobs: jobs:
cloc: cloc:
@@ -28,7 +28,6 @@ jobs:
- name: Print lines of code - name: Print lines of code
run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git run: cloc --include-lang TypeScript,JavaScript,HTML,Sass,CSS --vcs git
setup: setup:
name: Setup name: Setup
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -42,7 +41,6 @@ jobs:
id: version id: version
run: echo "::set-output name=value::${GITHUB_SHA:0:7}" run: echo "::set-output name=value::${GITHUB_SHA:0:7}"
build-oss-selfhost: build-oss-selfhost:
name: Build OSS zip name: Build OSS zip
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -53,13 +51,13 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: '16' node-version: "16"
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: '~/.npm' path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
@@ -84,13 +82,12 @@ jobs:
zip -r web-$_VERSION-selfhosted-open-source.zip build zip -r web-$_VERSION-selfhosted-open-source.zip build
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3 uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with: with:
name: web-${{ env._VERSION }}-selfhosted-open-source.zip name: web-${{ env._VERSION }}-selfhosted-open-source.zip
path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip path: ./web-${{ env._VERSION }}-selfhosted-open-source.zip
if-no-files-found: error if-no-files-found: error
build-cloud: build-cloud:
name: Build Cloud zip name: Build Cloud zip
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -101,13 +98,13 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: '16' node-version: "16"
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: '~/.npm' path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
@@ -132,13 +129,12 @@ jobs:
zip -r web-$_VERSION-cloud-COMMERCIAL.zip build zip -r web-$_VERSION-cloud-COMMERCIAL.zip build
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3 uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with: with:
name: web-${{ env._VERSION }}-cloud-COMMERCIAL.zip name: web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip path: ./web-${{ env._VERSION }}-cloud-COMMERCIAL.zip
if-no-files-found: error if-no-files-found: error
build-commercial-selfhost: build-commercial-selfhost:
name: Build SelfHost Docker image name: Build SelfHost Docker image
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -149,13 +145,13 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: '16' node-version: "16"
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: '~/.npm' path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
@@ -195,7 +191,7 @@ jobs:
zip -r web-$_VERSION-selfhosted-COMMERCIAL.zip build zip -r web-$_VERSION-selfhosted-COMMERCIAL.zip build
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3 uses: actions/upload-artifact@ee69f02b3dfdecd58bb31b4d133da38ba6fe3700 # v2.2.3
with: with:
name: web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip name: web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip
path: ./web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip path: ./web-${{ env._VERSION }}-selfhosted-COMMERCIAL.zip
@@ -248,7 +244,6 @@ jobs:
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix' if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix'
run: docker logout run: docker logout
build-qa: build-qa:
name: Build Docker images for QA environment name: Build Docker images for QA environment
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -256,13 +251,13 @@ jobs:
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: '16' node-version: "16"
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: '~/.npm' path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Print environment - name: Print environment
@@ -344,7 +339,6 @@ jobs:
- name: Log out of Docker - name: Log out of Docker
run: docker logout run: docker logout
windows: windows:
name: Test code on Windows name: Test code on Windows
runs-on: windows-2019 runs-on: windows-2019
@@ -352,22 +346,22 @@ jobs:
- name: Set up NuGet - name: Set up NuGet
uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1 uses: nuget/setup-nuget@04b0c2b8d1b97922f67eca497d7cf0bf17b8ffe1
with: with:
nuget-version: 'latest' nuget-version: "latest"
- name: Set up MSBuild - name: Set up MSBuild
uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d uses: microsoft/setup-msbuild@c26a08ba26249b81327e26f6ef381897b6a8754d
- name: Cache npm - name: Cache npm
id: npm-cache id: npm-cache
uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6 uses: actions/cache@c64c572235d810460d0d6876e9c705ad5002b353 # v2.1.6
with: with:
path: '~/.npm' path: "~/.npm"
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
- name: Set up Node - name: Set up Node
uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea uses: actions/setup-node@46071b5c7a2e0c34e49c3cb8a0e792e86e18d5ea
with: with:
node-version: '16' node-version: "16"
- name: Print environment - name: Print environment
run: | run: |
@@ -388,13 +382,12 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
# - name: Run linter - name: Run linter
# run: npm run lint run: npm run lint
- name: NPM build - name: NPM build
run: npm run build:bit:cloud run: npm run build:bit:cloud
crowdin-push: crowdin-push:
name: Crowdin Push name: Crowdin Push
if: github.ref == 'refs/heads/master' if: github.ref == 'refs/heads/master'
@@ -408,7 +401,7 @@ jobs:
_CROWDIN_PROJECT_ID: "308189" _CROWDIN_PROJECT_ID: "308189"
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Login to Azure - name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
@@ -423,7 +416,7 @@ jobs:
secrets: "crowdin-api-token" secrets: "crowdin-api-token"
- name: Upload Sources - name: Upload Sources
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}
@@ -433,7 +426,6 @@ jobs:
upload_sources: true upload_sources: true
upload_translations: false upload_translations: false
check-failures: check-failures:
name: Check for failures name: Check for failures
if: always() if: always()
@@ -493,7 +485,7 @@ jobs:
secrets: "devops-alerts-slack-webhook-url" secrets: "devops-alerts-slack-webhook-url"
- name: Notify Slack on failure - name: Notify Slack on failure
uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2 uses: act10ns/slack@e4e71685b9b239384b0f676a63c32367f59c2522 # v1.2.2
if: failure() if: failure()
env: env:
SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }}

View File

@@ -5,7 +5,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: {} inputs: {}
schedule: schedule:
- cron: '0 0 * * 5' - cron: "0 0 * * 5"
jobs: jobs:
crowdin-pull: crowdin-pull:
@@ -15,7 +15,7 @@ jobs:
_CROWDIN_PROJECT_ID: "308189" _CROWDIN_PROJECT_ID: "308189"
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Login to Azure - name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a
@@ -30,7 +30,7 @@ jobs:
secrets: "crowdin-api-token" secrets: "crowdin-api-token"
- name: Download translations - name: Download translations
uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2 uses: crowdin/github-action@e39093fd75daae7859c68eded4b43d42ec78d8ea # v1.3.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }} CROWDIN_API_TOKEN: ${{ steps.retrieve-secrets.outputs.crowdin-api-token }}

View File

@@ -20,11 +20,10 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup - name: Setup
run: run: export PATH=$PATH:~/work/web/web
export PATH=$PATH:~/work/web/web
- name: Login to Azure - name: Login to Azure
uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a uses: Azure/login@77f1b2e3fb80c0e8645114159d17008b8a2e475a

View File

@@ -24,7 +24,7 @@ jobs:
fi fi
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # 2.3.4
- name: Check Release Version - name: Check Release Version
id: version id: version
@@ -48,7 +48,6 @@ jobs:
BRANCH_NAME=$(basename ${{ github.ref }}) BRANCH_NAME=$(basename ${{ github.ref }})
echo "::set-output name=branch-name::$BRANCH_NAME" echo "::set-output name=branch-name::$BRANCH_NAME"
self-host: self-host:
name: Release self-host docker name: Release self-host docker
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -96,7 +95,6 @@ jobs:
- name: Log out of Docker - name: Log out of Docker
run: docker logout run: docker logout
ghpages-deploy: ghpages-deploy:
name: Deploy Web Vault name: Deploy Web Vault
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -108,7 +106,7 @@ jobs:
_TAG_VERSION: ${{ needs.setup.outputs.tag_version }} _TAG_VERSION: ${{ needs.setup.outputs.tag_version }}
steps: steps:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
with: with:
ref: gh-pages ref: gh-pages
@@ -118,7 +116,7 @@ jobs:
git push -u origin deploy-$_TAG_VERSION git push -u origin deploy-$_TAG_VERSION
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
- name: Setup git config - name: Setup git config
run: | run: |
@@ -140,7 +138,7 @@ jobs:
run: unzip web-*-cloud-COMMERCIAL.zip run: unzip web-*-cloud-COMMERCIAL.zip
- name: Deploy GitHub Pages - name: Deploy GitHub Pages
uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0 uses: crazy-max/ghaction-github-pages@db4476a01402e1a7ce05f41832040eef16d14925 # v2.5.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -159,7 +157,6 @@ jobs:
--base gh-pages \ --base gh-pages \
--head "$PR_BRANCH" --head "$PR_BRANCH"
release: release:
name: Create GitHub Release name: Create GitHub Release
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
@@ -175,7 +172,7 @@ jobs:
workflow_conclusion: success workflow_conclusion: success
branch: ${{ needs.setup.outputs.branch-name }} branch: ${{ needs.setup.outputs.branch-name }}
artifacts: "web-*-selfhosted-COMMERCIAL.zip, artifacts: "web-*-selfhosted-COMMERCIAL.zip,
web-*-selfhosted-open-source.zip" web-*-selfhosted-open-source.zip"
- name: Rename assets - name: Rename assets
run: | run: |
@@ -190,6 +187,6 @@ jobs:
tag: "${{ needs.setup.outputs.tag_version }}" tag: "${{ needs.setup.outputs.tag_version }}"
body: "<insert release notes here>" body: "<insert release notes here>"
artifacts: "web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip, artifacts: "web-${{ needs.setup.outputs.release_version }}-selfhosted-COMMERCIAL.zip,
web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip" web-${{ needs.setup.outputs.release_version }}-selfhosted-open-source.zip"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
draft: true draft: true

View File

@@ -6,17 +6,12 @@ Please visit our [Community Forums](https://community.bitwarden.com/) for genera
Here is how you can get involved: Here is how you can get involved:
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one - **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
- **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
* **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)
* **Report a bug or submit a bugfix:** Use Github issues and pull requests - **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
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
* **Translate:** See the localization (l10n) section below
## Contributor Agreement ## Contributor Agreement
@@ -24,9 +19,9 @@ Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web)
## Pull Request Guidelines ## Pull Request Guidelines
* use `npm run lint` and fix any linting suggestions before submitting a pull request - use `npm run lint` and fix any linting suggestions before submitting a pull request
* commit any pull requests against the `master` branch - commit any pull requests against the `master` branch
* include a link to your Community Forums post - include a link to your Community Forums post
# Localization (l10n) # Localization (l10n)

View File

@@ -48,16 +48,14 @@ You can also manually adjusting your API endpoint settings by adding `config/loc
```json ```json
{ {
"dev": { "dev": {
"proxyApi": "http://your-api-url", "proxyApi": "http://your-api-url",
"proxyIdentity": "http://your-identity-url", "proxyIdentity": "http://your-identity-url",
"proxyEvents": "http://your-events-url", "proxyEvents": "http://your-events-url",
"proxyNotifications": "http://your-notifications-url", "proxyNotifications": "http://your-notifications-url",
"allowedHosts": ["hostnames-to-allow-in-webpack"], "allowedHosts": ["hostnames-to-allow-in-webpack"]
}, },
"urls": { "urls": {}
}
} }
``` ```

View File

@@ -7,7 +7,7 @@ notify us. We welcome working with you to resolve the issue promptly. Thanks in
- Let us know as soon as possible upon discovery of a potential security issue, and we'll make every - Let us know as soon as possible upon discovery of a potential security issue, and we'll make every
effort to quickly resolve the issue. effort to quickly resolve the issue.
- Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a - Provide us a reasonable amount of time to resolve the issue before any disclosure to the public or a
third-party. We may publicly disclose the issue before resolving it, if appropriate. third-party. We may publicly disclose the issue before resolving it, if appropriate.
- Make a good faith effort to avoid privacy violations, destruction of data, and interruption or - Make a good faith effort to avoid privacy violations, destruction of data, and interruption or
degradation of our service. Only interact with accounts you own or with explicit permission of the degradation of our service. Only interact with accounts you own or with explicit permission of the
account holder. account holder.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,84 +1,82 @@
import { import { Component, OnInit } from "@angular/core";
Component, import { ActivatedRoute, Router } from "@angular/router";
OnInit,
} from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { ExportService } from 'jslib-common/abstractions/export.service'; import { ExportService } from "jslib-common/abstractions/export.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from 'jslib-common/abstractions/provider.service'; import { 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({ @Component({
selector: 'provider-events', selector: "provider-events",
templateUrl: 'events.component.html', templateUrl: "events.component.html",
}) })
export class EventsComponent extends BaseEventsComponent implements OnInit { export class EventsComponent extends BaseEventsComponent implements OnInit {
exportFileName: string = 'provider-events'; exportFileName: string = "provider-events";
providerId: string; providerId: string;
private providerUsersUserIdMap = new Map<string, any>(); private providerUsersUserIdMap = new Map<string, any>();
private providerUsersIdMap = new Map<string, any>(); private providerUsersIdMap = new Map<string, any>();
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private route: ActivatedRoute, private route: ActivatedRoute,
eventService: EventService, eventService: EventService,
i18nService: I18nService, i18nService: I18nService,
private providerService: ProviderService, private providerService: ProviderService,
exportService: ExportService, exportService: ExportService,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
private router: Router, private router: Router,
logService: LogService, logService: LogService,
private userNamePipe: UserNamePipe, private userNamePipe: UserNamePipe
) { ) {
super( super(eventService, i18nService, exportService, platformUtilsService, logService);
eventService, }
i18nService,
exportService,
platformUtilsService,
logService,
);
}
async ngOnInit() { async ngOnInit() {
this.route.parent.parent.params.subscribe(async params => { this.route.parent.parent.params.subscribe(async (params) => {
this.providerId = params.providerId; this.providerId = params.providerId;
const provider = await this.providerService.get(this.providerId); const provider = await this.providerService.get(this.providerId);
if (provider == null || !provider.useEvents) { if (provider == null || !provider.useEvents) {
this.router.navigate(['/providers', this.providerId]); this.router.navigate(["/providers", this.providerId]);
return; return;
} }
await this.load(); await this.load();
}); });
} }
async load() { async load() {
const response = await this.apiService.getProviderUsers(this.providerId); const response = await this.apiService.getProviderUsers(this.providerId);
response.data.forEach(u => { response.data.forEach((u) => {
const name = this.userNamePipe.transform(u); const name = this.userNamePipe.transform(u);
this.providerUsersIdMap.set(u.id, { name: name, email: u.email }); this.providerUsersIdMap.set(u.id, { name: name, email: u.email });
this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email }); this.providerUsersUserIdMap.set(u.userId, { name: name, email: u.email });
}); });
await this.loadEvents(true); await this.loadEvents(true);
this.loaded = true; this.loaded = true;
} }
protected requestEvents(startDate: string, endDate: string, continuationToken: string) { protected requestEvents(startDate: string, endDate: string, continuationToken: string) {
return this.apiService.getEventsProvider(this.providerId, startDate, endDate, continuationToken); return this.apiService.getEventsProvider(
} this.providerId,
startDate,
endDate,
continuationToken
);
}
protected getUserName(r: EventResponse, userId: string) { protected getUserName(r: EventResponse, userId: string) {
return userId != null && this.providerUsersUserIdMap.has(userId) ? this.providerUsersUserIdMap.get(userId) : null; 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="container page-content">
<div class="row"> <div class="row">
<div class="col-3"> <div class="col-3">
<div class="card" *ngIf="provider"> <div class="card" *ngIf="provider">
<div class="card-header">{{'manage' | i18n}}</div> <div class="card-header">{{ "manage" | i18n }}</div>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
<a routerLink="people" class="list-group-item" routerLinkActive="active" <a
*ngIf="provider.canManageUsers"> routerLink="people"
{{'people' | i18n}} class="list-group-item"
</a> routerLinkActive="active"
<a routerLink="events" class="list-group-item" routerLinkActive="active" *ngIf="provider.canManageUsers"
*ngIf="provider.canAccessEventLogs && accessEvents"> >
{{'eventLogs' | i18n}} {{ "people" | i18n }}
</a> </a>
</div> <a
</div> routerLink="events"
</div> class="list-group-item"
<div class="col-9"> routerLinkActive="active"
<router-outlet></router-outlet> *ngIf="provider.canAccessEventLogs && accessEvents"
>
{{ "eventLogs" | i18n }}
</a>
</div> </div>
</div>
</div> </div>
<div class="col-9">
<router-outlet></router-outlet>
</div>
</div>
</div> </div>

View File

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

View File

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

View File

@@ -1,257 +1,292 @@
import { import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
Component, import { ActivatedRoute, Router } from "@angular/router";
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 { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from 'jslib-common/abstractions/provider.service'; import { ProviderService } from "jslib-common/abstractions/provider.service";
import { SearchService } from 'jslib-common/abstractions/search.service'; import { SearchService } from "jslib-common/abstractions/search.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { ModalService } from 'jslib-angular/services/modal.service'; import { ModalService } from "jslib-angular/services/modal.service";
import { ValidationService } from 'jslib-angular/services/validation.service'; import { ValidationService } from "jslib-angular/services/validation.service";
import { ProviderUserStatusType } from 'jslib-common/enums/providerUserStatusType'; import { ProviderUserStatusType } from "jslib-common/enums/providerUserStatusType";
import { ProviderUserType } from 'jslib-common/enums/providerUserType'; import { ProviderUserType } from "jslib-common/enums/providerUserType";
import { SearchPipe } from 'jslib-angular/pipes/search.pipe'; import { SearchPipe } from "jslib-angular/pipes/search.pipe";
import { UserNamePipe } from 'jslib-angular/pipes/user-name.pipe'; import { UserNamePipe } from "jslib-angular/pipes/user-name.pipe";
import { ListResponse } from 'jslib-common/models/response/listResponse'; import { ListResponse } from "jslib-common/models/response/listResponse";
import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse'; import { ProviderUserUserDetailsResponse } from "jslib-common/models/response/provider/providerUserResponse";
import { ProviderUserBulkRequest } from 'jslib-common/models/request/provider/providerUserBulkRequest'; import { ProviderUserBulkRequest } from "jslib-common/models/request/provider/providerUserBulkRequest";
import { ProviderUserConfirmRequest } from 'jslib-common/models/request/provider/providerUserConfirmRequest'; import { ProviderUserConfirmRequest } from "jslib-common/models/request/provider/providerUserConfirmRequest";
import { ProviderUserBulkResponse } from 'jslib-common/models/response/provider/providerUserBulkResponse'; import { ProviderUserBulkResponse } from "jslib-common/models/response/provider/providerUserBulkResponse";
import { BasePeopleComponent } from 'src/app/common/base.people.component'; import { BasePeopleComponent } from "src/app/common/base.people.component";
import { BulkStatusComponent } from 'src/app/organizations/manage/bulk/bulk-status.component'; import { BulkStatusComponent } from "src/app/organizations/manage/bulk/bulk-status.component";
import { EntityEventsComponent } from 'src/app/organizations/manage/entity-events.component'; import { EntityEventsComponent } from "src/app/organizations/manage/entity-events.component";
import { BulkConfirmComponent } from './bulk/bulk-confirm.component'; import { BulkConfirmComponent } from "./bulk/bulk-confirm.component";
import { BulkRemoveComponent } from './bulk/bulk-remove.component'; import { BulkRemoveComponent } from "./bulk/bulk-remove.component";
import { UserAddEditComponent } from './user-add-edit.component'; import { UserAddEditComponent } from "./user-add-edit.component";
@Component({ @Component({
selector: 'provider-people', selector: "provider-people",
templateUrl: 'people.component.html', templateUrl: "people.component.html",
}) })
export class PeopleComponent 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; userType = ProviderUserType;
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef; userStatusType = ProviderUserStatusType;
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef; providerId: string;
@ViewChild('bulkStatusTemplate', { read: ViewContainerRef, static: true }) bulkStatusModalRef: ViewContainerRef; accessEvents = false;
@ViewChild('bulkConfirmTemplate', { read: ViewContainerRef, static: true }) bulkConfirmModalRef: ViewContainerRef;
@ViewChild('bulkRemoveTemplate', { read: ViewContainerRef, static: true }) bulkRemoveModalRef: ViewContainerRef;
userType = ProviderUserType; constructor(
userStatusType = ProviderUserStatusType; apiService: ApiService,
providerId: string; private route: ActivatedRoute,
accessEvents = false; 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( ngOnInit() {
apiService: ApiService, this.route.parent.params.subscribe(async (params) => {
private route: ActivatedRoute, this.providerId = params.providerId;
i18nService: I18nService, const provider = await this.providerService.get(this.providerId);
modalService: ModalService,
platformUtilsService: PlatformUtilsService,
cryptoService: CryptoService,
private router: Router,
searchService: SearchService,
validationService: ValidationService,
logService: LogService,
searchPipe: SearchPipe,
userNamePipe: UserNamePipe,
stateService: StateService,
private providerService: ProviderService,
) {
super(
apiService,
searchService,
i18nService,
platformUtilsService,
cryptoService,
validationService,
modalService,
logService,
searchPipe,
userNamePipe,
stateService,
);
}
ngOnInit() { if (!provider.canManageUsers) {
this.route.parent.params.subscribe(async params => { this.router.navigate(["../"], { relativeTo: this.route });
this.providerId = params.providerId; return;
const provider = await this.providerService.get(this.providerId); }
if (!provider.canManageUsers) { this.accessEvents = provider.useEvents;
this.router.navigate(['../'], { relativeTo: this.route });
return;
}
this.accessEvents = provider.useEvents; await this.load();
await this.load(); this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
this.searchText = qParams.search;
this.route.queryParams.pipe(first()).subscribe(async qParams => { if (qParams.viewEvents != null) {
this.searchText = qParams.search; const user = this.users.filter((u) => u.id === qParams.viewEvents);
if (qParams.viewEvents != null) { if (user.length > 0 && user[0].status === ProviderUserStatusType.Confirmed) {
const user = this.users.filter(u => u.id === qParams.viewEvents); this.events(user[0]);
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;
} }
});
});
}
const [modal] = await this.modalService.openViewRef(BulkRemoveComponent, this.bulkRemoveModalRef, comp => { getUsers(): Promise<ListResponse<ProviderUserUserDetailsResponse>> {
comp.providerId = this.providerId; return this.apiService.getProviderUsers(this.providerId);
comp.users = this.getCheckedUsers(); }
deleteUser(id: string): Promise<any> {
return this.apiService.deleteProviderUser(this.providerId, id);
}
reinviteUser(id: string): Promise<any> {
return this.apiService.postProviderUserReinvite(this.providerId, id);
}
async confirmUser(user: ProviderUserUserDetailsResponse, publicKey: Uint8Array): Promise<any> {
const providerKey = await this.cryptoService.getProviderKey(this.providerId);
const key = await this.cryptoService.rsaEncrypt(providerKey.key, publicKey.buffer);
const request = new ProviderUserConfirmRequest();
request.key = key.encryptedString;
await this.apiService.postProviderUserConfirm(this.providerId, user.id, request);
}
async edit(user: ProviderUserUserDetailsResponse) {
const [modal] = await this.modalService.openViewRef(
UserAddEditComponent,
this.addEditModalRef,
(comp) => {
comp.name = this.userNamePipe.transform(user);
comp.providerId = this.providerId;
comp.providerUserId = user != null ? user.id : null;
comp.onSavedUser.subscribe(() => {
modal.close();
this.load();
}); });
comp.onDeletedUser.subscribe(() => {
modal.close();
this.removeUser(user);
});
}
);
}
await modal.onClosedPromise(); async events(user: ProviderUserUserDetailsResponse) {
await this.load(); 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() { const [modal] = await this.modalService.openViewRef(
if (this.actionPromise != null) { BulkRemoveComponent,
return; this.bulkRemoveModalRef,
} (comp) => {
comp.providerId = this.providerId;
comp.users = this.getCheckedUsers();
}
);
const users = this.getCheckedUsers(); await modal.onClosedPromise();
const filteredUsers = users.filter(u => u.status === ProviderUserStatusType.Invited); await this.load();
}
if (filteredUsers.length <= 0) { async bulkReinvite() {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'), if (this.actionPromise != null) {
this.i18nService.t('noSelectedUsersApplicable')); return;
return;
}
try {
const request = new ProviderUserBulkRequest(filteredUsers.map(user => user.id));
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
this.showBulkStatus(users, filteredUsers, response, this.i18nService.t('bulkReinviteMessage'));
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
} }
async bulkConfirm() { const users = this.getCheckedUsers();
if (this.actionPromise != null) { const filteredUsers = users.filter((u) => u.status === ProviderUserStatusType.Invited);
return;
}
const [modal] = await this.modalService.openViewRef(BulkConfirmComponent, this.bulkConfirmModalRef, comp => { if (filteredUsers.length <= 0) {
comp.providerId = this.providerId; this.platformUtilsService.showToast(
comp.users = this.getCheckedUsers(); "error",
}); this.i18nService.t("errorOccurred"),
this.i18nService.t("noSelectedUsersApplicable")
await modal.onClosedPromise(); );
await this.load(); return;
} }
private async showBulkStatus(users: ProviderUserUserDetailsResponse[], filteredUsers: ProviderUserUserDetailsResponse[], try {
request: Promise<ListResponse<ProviderUserBulkResponse>>, successfullMessage: string) { const request = new ProviderUserBulkRequest(filteredUsers.map((user) => user.id));
const response = this.apiService.postManyProviderUserReinvite(this.providerId, request);
const [modal, childComponent] = await this.modalService.openViewRef(BulkStatusComponent, this.bulkStatusModalRef, comp => { this.showBulkStatus(
comp.loading = true; users,
}); filteredUsers,
response,
// Workaround to handle closing the modal shortly after it has been opened this.i18nService.t("bulkReinviteMessage")
let close = false; );
modal.onShown.subscribe(() => { } catch (e) {
if (close) { this.validationService.showError(e);
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();
}
} }
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 fade" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document"> <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate> <form
<div class="modal-header"> class="modal-content"
<h2 class="modal-title" id="userAddEditTitle"> #form
{{title}} (ngSubmit)="submit()"
<small class="text-muted" *ngIf="name">{{name}}</small> [appApiAction]="formPromise"
</h2> ngNativeValidate
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}"> >
<span aria-hidden="true">&times;</span> <div class="modal-header">
</button> <h2 class="modal-title" id="userAddEditTitle">
</div> {{ title }}
<div class="modal-body" *ngIf="loading"> <small class="text-muted" *ngIf="name">{{ name }}</small>
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i> </h2>
<span class="sr-only">{{'loading' | i18n}}</span> <button
</div> type="button"
<div class="modal-body" *ngIf="!loading"> class="close"
<ng-container *ngIf="!editMode"> data-dismiss="modal"
<p>{{'providerInviteUserDesc' | i18n}}</p> appA11yTitle="{{ 'close' | i18n }}"
<div class="form-group mb-4"> >
<label for="emails">{{'email' | i18n}}</label> <span aria-hidden="true">&times;</span>
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required </button>
appAutoFocus> </div>
<small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small> <div class="modal-body" *ngIf="loading">
</div> <i
</ng-container> class="fa fa-spinner fa-spin text-muted"
<h3> title="{{ 'loading' | i18n }}"
{{'userType' | i18n}} aria-hidden="true"
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}" ></i>
href="https://bitwarden.com/help/article/user-types-access-control/#user-types"> <span class="sr-only">{{ "loading" | i18n }}</span>
<i class="fa fa-question-circle-o" aria-hidden="true"></i> </div>
</a> <div class="modal-body" *ngIf="!loading">
</h3> <ng-container *ngIf="!editMode">
<div class="form-check mt-2 form-check-block"> <p>{{ "providerInviteUserDesc" | i18n }}</p>
<input class="form-check-input" type="radio" name="userType" id="userTypeServiceUser" <div class="form-group mb-4">
[value]="userType.ServiceUser" [(ngModel)]="type"> <label for="emails">{{ "email" | i18n }}</label>
<label class="form-check-label" for="userTypeServiceUser"> <input
{{'serviceUser' | i18n}} id="emails"
<small>{{'serviceUserDesc' | i18n}}</small> class="form-control"
</label> type="text"
</div> name="Emails"
<div class="form-check mt-2 form-check-block"> [(ngModel)]="emails"
<input class="form-check-input" type="radio" name="userType" id="userTypeProviderAdmin" required
[value]="userType.ProviderAdmin" [(ngModel)]="type"> appAutoFocus
<label class="form-check-label" for="userTypeProviderAdmin"> />
{{'providerAdmin' | i18n}} <small class="text-muted">{{ "inviteMultipleEmailDesc" | i18n: "20" }}</small>
<small>{{'providerAdminDesc' | i18n}}</small> </div>
</label> </ng-container>
</div> <h3>
</div> {{ "userType" | i18n }}
<div class="modal-footer"> <a
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> target="_blank"
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i> rel="noopener"
<span>{{'save' | i18n}}</span> appA11yTitle="{{ 'learnMore' | i18n }}"
</button> href="https://bitwarden.com/help/article/user-types-access-control/#user-types"
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal"> >
{{'cancel' | i18n}} <i class="fa fa-question-circle-o" aria-hidden="true"></i>
</button> </a>
<div class="ml-auto"> </h3>
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger" <div class="form-check mt-2 form-check-block">
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode" [disabled]="deleteBtn.loading" <input
[appApiAction]="deletePromise"> class="form-check-input"
<i class="fa fa-trash-o fa-lg fa-fw" [hidden]="deleteBtn.loading" aria-hidden="true"></i> type="radio"
<i class="fa fa-spinner fa-spin fa-lg fa-fw" [hidden]="!deleteBtn.loading" name="userType"
title="{{'loading' | i18n}}" aria-hidden="true"></i> id="userTypeServiceUser"
</button> [value]="userType.ServiceUser"
</div> [(ngModel)]="type"
</div> />
</form> <label class="form-check-label" for="userTypeServiceUser">
</div> {{ "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> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +1,31 @@
import { Injectable } from '@angular/core'; import { Injectable } from "@angular/core";
import { import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
ActivatedRouteSnapshot,
CanActivate,
Router,
} from '@angular/router';
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { ProviderService } from 'jslib-common/abstractions/provider.service'; import { ProviderService } from "jslib-common/abstractions/provider.service";
@Injectable() @Injectable()
export class ProviderGuardService implements CanActivate { export class ProviderGuardService implements CanActivate {
constructor( constructor(
private router: Router, private router: Router,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private i18nService: I18nService,
private providerService: ProviderService, private providerService: ProviderService
) { } ) {}
async canActivate(route: ActivatedRouteSnapshot) { async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.providerService.get(route.params.providerId); const provider = await this.providerService.get(route.params.providerId);
if (provider == null) { if (provider == null) {
this.router.navigate(['/']); this.router.navigate(["/"]);
return false; return false;
}
if (!provider.isProviderAdmin && !provider.enabled) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('providerIsDisabled'));
this.router.navigate(['/']);
return false;
}
return true;
} }
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 { Injectable } from "@angular/core";
import { import { ActivatedRouteSnapshot, CanActivate, Router } from "@angular/router";
ActivatedRouteSnapshot,
CanActivate,
Router,
} from '@angular/router';
import { ProviderService } from 'jslib-common/abstractions/provider.service'; import { ProviderService } from "jslib-common/abstractions/provider.service";
import { Permissions } from 'jslib-common/enums/permissions'; import { Permissions } from "jslib-common/enums/permissions";
@Injectable() @Injectable()
export class ProviderTypeGuardService implements CanActivate { export class ProviderTypeGuardService implements CanActivate {
constructor(private providerService: ProviderService, private router: Router) { } constructor(private providerService: ProviderService, private router: Router) {}
async canActivate(route: ActivatedRouteSnapshot) { async canActivate(route: ActivatedRouteSnapshot) {
const provider = await this.providerService.get(route.params.providerId); const provider = await this.providerService.get(route.params.providerId);
const permissions = route.data == null ? null : route.data.permissions as Permissions[]; const permissions = route.data == null ? null : (route.data.permissions as Permissions[]);
if ( if (
(permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) || (permissions.indexOf(Permissions.AccessEventLogs) !== -1 && provider.canAccessEventLogs) ||
(permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) || (permissions.indexOf(Permissions.ManageProvider) !== -1 && provider.isProviderAdmin) ||
(permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers) (permissions.indexOf(Permissions.ManageUsers) !== -1 && provider.canManageUsers)
) { ) {
return true; return true;
}
this.router.navigate(['/providers', provider.id]);
return false;
} }
this.router.navigate(["/providers", provider.id]);
return false;
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,95 +1,101 @@
import { import { Component, OnInit } from "@angular/core";
Component, import { ActivatedRoute, Router } from "@angular/router";
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 { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { ValidationService } from 'jslib-angular/services/validation.service'; import { ValidationService } from "jslib-angular/services/validation.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { ProviderSetupRequest } from 'jslib-common/models/request/provider/providerSetupRequest'; import { ProviderSetupRequest } from "jslib-common/models/request/provider/providerSetupRequest";
@Component({ @Component({
selector: 'provider-setup', selector: "provider-setup",
templateUrl: 'setup.component.html', templateUrl: "setup.component.html",
}) })
export class SetupComponent implements OnInit { export class SetupComponent implements OnInit {
loading = true; loading = true;
authed = false; authed = false;
email: string; email: string;
formPromise: Promise<any>; formPromise: Promise<any>;
providerId: string; providerId: string;
token: string; token: string;
name: string; name: string;
billingEmail: string; billingEmail: string;
constructor(private router: Router, private platformUtilsService: PlatformUtilsService, constructor(
private i18nService: I18nService, private route: ActivatedRoute, private router: Router,
private cryptoService: CryptoService, private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
private syncService: SyncService, private validationService: ValidationService) { } private i18nService: I18nService,
private route: ActivatedRoute,
private cryptoService: CryptoService,
private apiService: ApiService,
private syncService: SyncService,
private validationService: ValidationService
) {}
ngOnInit() { ngOnInit() {
document.body.classList.remove('layout_frontend'); document.body.classList.remove("layout_frontend");
this.route.queryParams.pipe(first()).subscribe(async qParams => { this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
const error = qParams.providerId == null || qParams.email == null || qParams.token == null; const error = qParams.providerId == null || qParams.email == null || qParams.token == null;
if (error) { if (error) {
this.platformUtilsService.showToast('error', null, this.i18nService.t('emergencyInviteAcceptFailed'), this.platformUtilsService.showToast(
{ timeout: 10000 }); "error",
this.router.navigate(['/']); null,
return; this.i18nService.t("emergencyInviteAcceptFailed"),
} {
timeout: 10000,
}
);
this.router.navigate(["/"]);
return;
}
this.providerId = qParams.providerId; this.providerId = qParams.providerId;
this.token = qParams.token; this.token = qParams.token;
// Check if provider exists, redirect if it does // Check if provider exists, redirect if it does
try { try {
const provider = await this.apiService.getProvider(this.providerId); const provider = await this.apiService.getProvider(this.providerId);
if (provider.name != null) { if (provider.name != null) {
this.router.navigate(['/providers', provider.id], { replaceUrl: true }); this.router.navigate(["/providers", provider.id], { replaceUrl: true });
}
} catch (e) {
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);
} }
} 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 { AngularWebpackPlugin } = require('@ngtools/webpack'); 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.entry["app/main"] = "./bitwarden_license/src/app/main.ts";
webpackConfig.plugins[webpackConfig.plugins.length -1] = new AngularWebpackPlugin({ webpackConfig.plugins[webpackConfig.plugins.length - 1] = new AngularWebpackPlugin({
tsConfigPath: 'tsconfig.json', tsConfigPath: "tsconfig.json",
entryModule: 'bitwarden_license/src/app/app.module#AppModule', entryModule: "bitwarden_license/src/app/app.module#AppModule",
sourceMap: true, sourceMap: true,
}); });
module.exports = webpackConfig; module.exports = webpackConfig;

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,11 @@
{ {
"urls": { "urls": {
"notifications": "http://localhost:61840" "notifications": "http://localhost:61840"
}, },
"dev": { "dev": {
"proxyApi": "http://localhost:4000", "proxyApi": "http://localhost:4000",
"proxyIdentity": "http://localhost:33656", "proxyIdentity": "http://localhost:33656",
"proxyEvents": "http://localhost:46273", "proxyEvents": "http://localhost:46273",
"proxyNotifications": "http://localhost:61840" "proxyNotifications": "http://localhost:61840"
} }
} }

View File

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

View File

@@ -29,7 +29,7 @@
"dist:bit:selfhost": "npm run build:bit:selfhost:prod", "dist:bit:selfhost": "npm run build:bit:selfhost:prod",
"deploy": "npm run dist:bit && gh-pages -d build", "deploy": "npm run dist:bit && gh-pages -d build",
"deploy:dev": "npm run dist:bit && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git", "deploy:dev": "npm run dist:bit && gh-pages -d build -r git@github.com:kspearrin/bitwarden-web-dev.git",
"lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts'", "lint": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' && prettier --check .",
"lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix", "lint:fix": "tslint 'src/**/*.ts' 'bitwarden_license/src/**/*.ts' --fix",
"prettier": "prettier --write .", "prettier": "prettier --write .",
"prepare": "husky install" "prepare": "husky install"

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -1,119 +1,121 @@
@font-face { @font-face {
font-family: 'Open Sans'; font-family: "Open Sans";
font-style: italic; font-style: italic;
font-weight: 300; font-weight: 300;
src: url(../fonts/Open_Sans-italic-300.woff) format('woff'); src: url(../fonts/Open_Sans-italic-300.woff) format("woff");
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: "Open Sans";
font-style: italic; font-style: italic;
font-weight: 400; font-weight: 400;
src: url(../fonts/Open_Sans-italic-400.woff) format('woff'); src: url(../fonts/Open_Sans-italic-400.woff) format("woff");
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: "Open Sans";
font-style: italic; font-style: italic;
font-weight: 600; font-weight: 600;
src: url(../fonts/Open_Sans-italic-600.woff) format('woff'); src: url(../fonts/Open_Sans-italic-600.woff) format("woff");
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: "Open Sans";
font-style: italic; font-style: italic;
font-weight: 700; font-weight: 700;
src: url(../fonts/Open_Sans-italic-700.woff) format('woff'); src: url(../fonts/Open_Sans-italic-700.woff) format("woff");
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: "Open Sans";
font-style: italic; font-style: italic;
font-weight: 800; font-weight: 800;
src: url(../fonts/Open_Sans-italic-800.woff) format('woff'); src: url(../fonts/Open_Sans-italic-800.woff) format("woff");
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: "Open Sans";
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: url(../fonts/Open_Sans-normal-300.woff) format('woff'); src: url(../fonts/Open_Sans-normal-300.woff) format("woff");
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: "Open Sans";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url(../fonts/Open_Sans-normal-400.woff) format('woff'); src: url(../fonts/Open_Sans-normal-400.woff) format("woff");
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: "Open Sans";
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
src: url(../fonts/Open_Sans-normal-600.woff) format('woff'); src: url(../fonts/Open_Sans-normal-600.woff) format("woff");
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: "Open Sans";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: url(../fonts/Open_Sans-normal-700.woff) format('woff'); src: url(../fonts/Open_Sans-normal-700.woff) format("woff");
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
@font-face { @font-face {
font-family: 'Open Sans'; font-family: "Open Sans";
font-style: normal; font-style: normal;
font-weight: 800; font-weight: 800;
src: url(../fonts/Open_Sans-normal-800.woff) format('woff'); src: url(../fonts/Open_Sans-normal-800.woff) format("woff");
unicode-range: U+0-10FFFF; unicode-range: U+0-10FFFF;
} }
body { body {
font-family: 'Open Sans'; font-family: "Open Sans";
} }
html, body, .row { html,
height: 100%; body,
-webkit-font-smoothing: antialiased; .row {
height: 100%;
-webkit-font-smoothing: antialiased;
} }
h2 { h2 {
font-size: 25px; font-size: 25px;
margin-bottom: 12.5px; margin-bottom: 12.5px;
font-weight: 500; font-weight: 500;
line-height: 1.1; line-height: 1.1;
} }
.brand { .brand {
font-size: 23px; font-size: 23px;
line-height: 25px; line-height: 25px;
color: #fff; color: #fff;
font-family: "Open Sans","Helvetica Neue",Helvetica,Arial,sans-serif; font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
} }
.banner { .banner {
background-color: #175DDC; background-color: #175ddc;
height: 56px; height: 56px;
} }
.content { .content {
padding-top: 20px; padding-top: 20px;
padding-bottom: 20px; padding-bottom: 20px;
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
} }
.footer { .footer {
padding: 40px 0 40px 0; padding: 40px 0 40px 0;
border-top: 1px solid #dee2e6; border-top: 1px solid #dee2e6;
} }

View File

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

View File

@@ -1,60 +1,54 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { EmergencyAccessAcceptRequest } from 'jslib-common/models/request/emergencyAccessAcceptRequest'; import { EmergencyAccessAcceptRequest } from "jslib-common/models/request/emergencyAccessAcceptRequest";
import { BaseAcceptComponent } from '../common/base.accept.component'; import { BaseAcceptComponent } from "../common/base.accept.component";
@Component({ @Component({
selector: 'app-accept-emergency', selector: "app-accept-emergency",
templateUrl: 'accept-emergency.component.html', templateUrl: "accept-emergency.component.html",
}) })
export class AcceptEmergencyComponent extends BaseAcceptComponent { export class AcceptEmergencyComponent extends BaseAcceptComponent {
name: string;
name: string; protected requiredParameters: string[] = ["id", "name", "email", "token"];
protected failedShortMessage = "emergencyInviteAcceptFailedShort";
protected failedMessage = "emergencyInviteAcceptFailed";
protected requiredParameters: string[] = ['id', 'name', 'email', 'token']; constructor(
protected failedShortMessage = 'emergencyInviteAcceptFailedShort'; router: Router,
protected failedMessage = 'emergencyInviteAcceptFailed'; platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
route: ActivatedRoute,
private apiService: ApiService,
stateService: StateService
) {
super(router, platformUtilsService, i18nService, route, stateService);
}
constructor( async authedHandler(qParams: any): Promise<void> {
router: Router, const request = new EmergencyAccessAcceptRequest();
platformUtilsService: PlatformUtilsService, request.token = qParams.token;
i18nService: I18nService, this.actionPromise = this.apiService.postEmergencyAccessAccept(qParams.id, request);
route: ActivatedRoute, await this.actionPromise;
private apiService: ApiService, this.platformUtilService.showToast(
stateService: StateService "success",
) { this.i18nService.t("inviteAccepted"),
super( this.i18nService.t("emergencyInviteAcceptedDesc"),
router, { timeout: 10000 }
platformUtilsService, );
i18nService, this.router.navigate(["/vault"]);
route, }
stateService
); async unauthedHandler(qParams: any): Promise<void> {
} this.name = qParams.name;
if (this.name != null) {
async authedHandler(qParams: any): Promise<void> { // Fix URL encoding of space issue with Angular
const request = new EmergencyAccessAcceptRequest(); this.name = this.name.replace(/\+/g, " ");
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 class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div> <div>
<img src="../../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden"> <img src="../../images/logo-dark@2x.png" class="mb-4 logo" alt="Bitwarden" />
<p class="text-center"> <p class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i> <i
<span class="sr-only">{{'loading' | i18n}}</span> class="fa fa-spinner fa-spin fa-2x text-muted"
</p> title="{{ 'loading' | i18n }}"
</div> aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</div>
</div> </div>
<div class="container" *ngIf="!loading && !authed"> <div class="container" *ngIf="!loading && !authed">
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="lead text-center mb-4">{{'joinOrganization' | i18n}}</p> <p class="lead text-center mb-4">{{ "joinOrganization" | i18n }}</p>
<div class="card d-block"> <div class="card d-block">
<div class="card-body"> <div class="card-body">
<p class="text-center"> <p class="text-center">
{{orgName}} {{ orgName }}
<strong class="d-block mt-2">{{email}}</strong> <strong class="d-block mt-2">{{ email }}</strong>
</p> </p>
<p>{{'joinOrganizationDesc' | i18n}}</p> <p>{{ "joinOrganizationDesc" | i18n }}</p>
<hr> <hr />
<div class="d-flex"> <div class="d-flex">
<a routerLink="/" [queryParams]="{email: email}" class="btn btn-primary btn-block"> <a routerLink="/" [queryParams]="{ email: email }" class="btn btn-primary btn-block">
{{'logIn' | i18n}} {{ "logIn" | i18n }}
</a> </a>
<a routerLink="/register" [queryParams]="{email: email}" <a
class="btn btn-primary btn-block ml-2 mt-0"> routerLink="/register"
{{'createAccount' | i18n}} [queryParams]="{ email: email }"
</a> class="btn btn-primary btn-block ml-2 mt-0"
</div> >
</div> {{ "createAccount" | i18n }}
</div> </a>
</div>
</div> </div>
</div>
</div> </div>
</div>
</div> </div>

View File

@@ -1,116 +1,127 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { OrganizationUserAcceptRequest } from 'jslib-common/models/request/organizationUserAcceptRequest'; import { OrganizationUserAcceptRequest } from "jslib-common/models/request/organizationUserAcceptRequest";
import { OrganizationUserResetPasswordEnrollmentRequest } from 'jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest'; import { OrganizationUserResetPasswordEnrollmentRequest } from "jslib-common/models/request/organizationUserResetPasswordEnrollmentRequest";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
import { Policy } from 'jslib-common/models/domain/policy'; import { Policy } from "jslib-common/models/domain/policy";
import { BaseAcceptComponent } from '../common/base.accept.component'; import { BaseAcceptComponent } from "../common/base.accept.component";
@Component({ @Component({
selector: 'app-accept-organization', selector: "app-accept-organization",
templateUrl: 'accept-organization.component.html', templateUrl: "accept-organization.component.html",
}) })
export class AcceptOrganizationComponent extends BaseAcceptComponent { export class AcceptOrganizationComponent extends BaseAcceptComponent {
orgName: string; orgName: string;
protected requiredParameters: string[] = ['organizationId', 'organizationUserId', 'token']; protected requiredParameters: string[] = ["organizationId", "organizationUserId", "token"];
constructor( constructor(
router: Router, router: Router,
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
i18nService: I18nService, i18nService: I18nService,
route: ActivatedRoute, route: ActivatedRoute,
private apiService: ApiService, private apiService: ApiService,
stateService: StateService, stateService: StateService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private policyService: PolicyService, private policyService: PolicyService,
private logService: LogService private logService: LogService
) { ) {
super( super(router, platformUtilsService, i18nService, route, stateService);
router, }
platformUtilsService,
i18nService, async authedHandler(qParams: any): Promise<void> {
route, const request = new OrganizationUserAcceptRequest();
stateService 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> { await this.actionPromise;
const request = new OrganizationUserAcceptRequest(); this.platformUtilService.showToast(
request.token = qParams.token; "success",
if (await this.performResetPasswordAutoEnroll(qParams)) { this.i18nService.t("inviteAccepted"),
this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId, this.i18nService.t("inviteAcceptedDesc"),
qParams.organizationUserId, request).then(() => { { timeout: 10000 }
// 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); await this.stateService.setOrganizationInvitation(null);
this.router.navigate(["/vault"]);
}
// RSA Encrypt user's encKey.key with organization public key async unauthedHandler(qParams: any): Promise<void> {
const encKey = await this.cryptoService.getEncKey(); this.orgName = qParams.organizationName;
const encryptedKey = await this.cryptoService.rsaEncrypt(encKey.key, publicKey.buffer); 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 private async performResetPasswordAutoEnroll(qParams: any): Promise<boolean> {
const resetRequest = new OrganizationUserResetPasswordEnrollmentRequest(); let policyList: Policy[] = null;
resetRequest.resetPasswordKey = encryptedKey.encryptedString; try {
const policies = await this.apiService.getPoliciesByToken(
return this.apiService.putOrganizationUserResetPasswordEnrollment(qParams.organizationId, await this.stateService.getUserId(), resetRequest); qParams.organizationId,
}); qParams.token,
} else { qParams.email,
this.actionPromise = this.apiService.postOrganizationUserAccept(qParams.organizationId, qParams.organizationUserId
qParams.organizationUserId, request); );
} policyList = this.policyService.mapPoliciesFromToken(policies);
} catch (e) {
await this.actionPromise; this.logService.error(e);
this.platformUtilService.showToast('success', this.i18nService.t('inviteAccepted'),
this.i18nService.t('inviteAcceptedDesc'), {timeout: 10000});
await this.stateService.setOrganizationInvitation(null);
this.router.navigate(['/vault']);
} }
async unauthedHandler(qParams: any): Promise<void> { if (policyList != null) {
this.orgName = qParams.organizationName; const result = this.policyService.getResetPasswordPolicyOptions(
if (this.orgName != null) { policyList,
// Fix URL encoding of space issue with Angular qParams.organizationId
this.orgName = this.orgName.replace(/\+/g, ' '); );
} // Return true if policy enabled and auto-enroll enabled
await this.stateService.setOrganizationInvitation(qParams); return result[1] && result[0].autoEnrollEnabled;
} }
private async performResetPasswordAutoEnroll(qParams: any): Promise<boolean> { return false;
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;
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,36 +1,43 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { Router } from '@angular/router'; import { Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { DeleteRecoverRequest } from 'jslib-common/models/request/deleteRecoverRequest'; import { DeleteRecoverRequest } from "jslib-common/models/request/deleteRecoverRequest";
@Component({ @Component({
selector: 'app-recover-delete', selector: "app-recover-delete",
templateUrl: 'recover-delete.component.html', templateUrl: "recover-delete.component.html",
}) })
export class RecoverDeleteComponent { export class RecoverDeleteComponent {
email: string; email: string;
formPromise: Promise<any>; formPromise: Promise<any>;
constructor(private router: Router, private apiService: ApiService, constructor(
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private router: Router,
private logService: LogService) { private apiService: ApiService,
} private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private logService: LogService
) {}
async submit() { async submit() {
try { try {
const request = new DeleteRecoverRequest(); const request = new DeleteRecoverRequest();
request.email = this.email.trim().toLowerCase(); request.email = this.email.trim().toLowerCase();
this.formPromise = this.apiService.postAccountRecoverDelete(request); this.formPromise = this.apiService.postAccountRecoverDelete(request);
await this.formPromise; await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('deleteRecoverEmailSent')); this.platformUtilsService.showToast(
this.router.navigate(['/']); "success",
} catch (e) { null,
this.logService.error(e); 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> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="lead text-center mb-4">{{'recoverAccountTwoStep' | i18n}}</p> <p class="lead text-center mb-4">{{ "recoverAccountTwoStep" | i18n }}</p>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<p>{{'recoverAccountTwoStepDesc' | i18n}} <p>
<a href="https://help.bitwarden.com/article/lost-two-step-device/" target="_blank" {{ "recoverAccountTwoStepDesc" | i18n }}
rel="noopener">{{'learnMore' | i18n}}</a> <a
</p> href="https://help.bitwarden.com/article/lost-two-step-device/"
<div class="form-group"> target="_blank"
<label for="email">{{'emailAddress' | i18n}}</label> rel="noopener"
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email" required >{{ "learnMore" | i18n }}</a
appAutofocus inputmode="email" appInputVerbatim="false"> >
</div> </p>
<div class="form-group"> <div class="form-group">
<label for="masterPassword">{{'masterPass' | i18n}}</label> <label for="email">{{ "emailAddress" | i18n }}</label>
<input id="masterPassword" type="password" name="MasterPassword" class="form-control" <input
[(ngModel)]="masterPassword" required appInputVerbatim> id="email"
</div> class="form-control"
<div class="form-group"> type="text"
<label for="recoveryCode">{{'recoveryCodeTitle' | i18n}}</label> name="Email"
<input id="recoveryCode" class="text-monospace form-control" type="text" name="RecoveryCode" [(ngModel)]="email"
[(ngModel)]="recoveryCode" required appInputVerbatim> required
</div> appAutofocus
<hr> inputmode="email"
<div class="d-flex"> appInputVerbatim="false"
<button type="submit" class="btn btn-primary btn-block btn-submit" [disabled]="form.loading"> />
<span>{{'submit' | i18n}}</span> </div>
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i> <div class="form-group">
</button> <label for="masterPassword">{{ "masterPass" | i18n }}</label>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0"> <input
{{'cancel' | i18n}} id="masterPassword"
</a> type="password"
</div> name="MasterPassword"
</div> class="form-control"
</div> [(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>
</div> </div>
</div>
</form> </form>

View File

@@ -1,43 +1,52 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { Router } from '@angular/router'; import { Router } from "@angular/router";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { TwoFactorRecoveryRequest } from 'jslib-common/models/request/twoFactorRecoveryRequest'; import { TwoFactorRecoveryRequest } from "jslib-common/models/request/twoFactorRecoveryRequest";
@Component({ @Component({
selector: 'app-recover-two-factor', selector: "app-recover-two-factor",
templateUrl: 'recover-two-factor.component.html', templateUrl: "recover-two-factor.component.html",
}) })
export class RecoverTwoFactorComponent { export class RecoverTwoFactorComponent {
email: string; email: string;
masterPassword: string; masterPassword: string;
recoveryCode: string; recoveryCode: string;
formPromise: Promise<any>; formPromise: Promise<any>;
constructor(private router: Router, private apiService: ApiService, constructor(
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private router: Router,
private cryptoService: CryptoService, private authService: AuthService, private apiService: ApiService,
private logService: LogService) { } private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private cryptoService: CryptoService,
private authService: AuthService,
private logService: LogService
) {}
async submit() { async submit() {
try { try {
const request = new TwoFactorRecoveryRequest(); const request = new TwoFactorRecoveryRequest();
request.recoveryCode = this.recoveryCode.replace(/\s/g, '').toLowerCase(); request.recoveryCode = this.recoveryCode.replace(/\s/g, "").toLowerCase();
request.email = this.email.trim().toLowerCase(); request.email = this.email.trim().toLowerCase();
const key = await this.authService.makePreloginKey(this.masterPassword, request.email); const key = await this.authService.makePreloginKey(this.masterPassword, request.email);
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key); request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, key);
this.formPromise = this.apiService.postTwoFactorRecover(request); this.formPromise = this.apiService.postTwoFactorRecover(request);
await this.formPromise; await this.formPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('twoStepRecoverDisabled')); this.platformUtilsService.showToast(
this.router.navigate(['/']); "success",
} catch (e) { null,
this.logService.error(e); 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]"> <div class="layout" [ngClass]="['layout', layout]">
<header class="header" *ngIf="layout === 'enterprise2'"> <header class="header" *ngIf="layout === 'enterprise2'">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-7"> <div class="col-7">
<img alt="Bitwarden" class="logo mb-2" src="../../images/register-layout/logo-horizontal-white.png"> <img
</div> alt="Bitwarden"
</div> class="logo mb-2"
src="../../images/register-layout/logo-horizontal-white.png"
/>
</div> </div>
</header> </div>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate> </div>
<div class="row"> </header>
<div class="col-7" *ngIf="layout"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="mt-5"> <div class="row">
<div *ngIf="layout === 'enterprise2'"> <div class="col-7" *ngIf="layout">
<h2>Companies globally trust Bitwarden for password management.</h2> <div class="mt-5">
<p>Start your 7-day free trial!</p> <div *ngIf="layout === 'enterprise2'">
<p class="highlight">Quickly deploy your <b>organization</b></p> <h2>Companies globally trust Bitwarden for password management.</h2>
<p>Use Bitwarden across all platforms</p> <p>Start your 7-day free trial!</p>
<p>Collaborate and share securely</p> <p class="highlight">Quickly deploy your <b>organization</b></p>
<figure> <p>Use Bitwarden across all platforms</p>
<figcaption> <p>Collaborate and share securely</p>
<cite> <figure>
<img src="../../images/register-layout/wired-logo.png" alt="Wired"> <figcaption>
</cite> <cite>
</figcaption> <img src="../../images/register-layout/wired-logo.png" alt="Wired" />
<blockquote> </cite>
"Bitwarden has become a popular choice among open-source software advocates. After using </figcaption>
it for a few months, I can see why." - February 2020 <blockquote>
</blockquote> "Bitwarden has become a popular choice among open-source software advocates. After
</figure> using it for a few months, I can see why." - February 2020
</div> </blockquote>
<div *ngIf="layout === 'enterprise3'"> </figure>
<p>Enterprise 3 layout</p> </div>
</div> <div *ngIf="layout === 'enterprise3'">
<div *ngIf="layout === 'enterprise4'"> <p>Enterprise 3 layout</p>
<p>Enterprise 4 layout</p> </div>
</div> <div *ngIf="layout === 'enterprise4'">
</div> <p>Enterprise 4 layout</p>
</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>
</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="fa-thumb-tack"
*ngIf="showCreateOrgMessage"
>
{{ "createOrganizationCreatePersonalAccount" | i18n }}
</app-callout>
<div class="form-group">
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
class="form-control"
type="text"
name="Email"
[(ngModel)]="email"
required
[appAutofocus]="email === ''"
inputmode="email"
appInputVerbatim="false"
/>
<small class="form-text text-muted">{{ "emailAddressDesc" | i18n }}</small>
</div>
<div class="form-group">
<label for="name">{{ "yourName" | i18n }}</label>
<input
id="name"
class="form-control"
type="text"
name="Name"
[(ngModel)]="name"
[appAutofocus]="email !== ''"
/>
<small class="form-text text-muted">{{ "yourNameDesc" | i18n }}</small>
</div>
<div class="form-group">
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<div class="d-flex">
<div class="w-100">
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="text-monospace form-control mb-1"
[(ngModel)]="masterPassword"
(input)="updatePasswordStrength()"
required
appInputVerbatim
/>
<app-password-strength [score]="masterPasswordScore" [showText]="true">
</app-password-strength>
</div>
<div>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(false)"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{
'fa-eye': !showPassword,
'fa-eye-slash': showPassword
}"
></i>
</button>
<div class="progress-bar invisible"></div>
</div>
</div>
<small class="form-text text-muted">{{ "masterPassDesc" | i18n }}</small>
</div>
<div class="form-group">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<div class="d-flex">
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="text-monospace form-control"
[(ngModel)]="confirmMasterPassword"
required
appInputVerbatim
/>
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword(true)"
>
<i
class="fa fa-lg"
aria-hidden="true"
[ngClass]="{ 'fa-eye': !showPassword, 'fa-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="form-group">
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input
id="hint"
class="form-control"
type="text"
name="Hint"
[(ngModel)]="hint"
/>
<small class="form-text text-muted">{{ "masterPassHintDesc" | i18n }}</small>
</div>
<div [hidden]="!showCaptcha()">
<iframe id="hcaptcha_iframe" height="80"></iframe>
</div>
<div class="form-group" *ngIf="showTerms">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="acceptPolicies"
[(ngModel)]="acceptPolicies"
name="AcceptPolicies"
/>
<label class="form-check-label small text-muted" for="acceptPolicies">
{{ "acceptPolicies" | i18n }}<br />
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{
"termsOfService" | i18n
}}</a
>,
<a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{
"privacyPolicy" | i18n
}}</a>
</label>
</div>
</div>
<hr />
<div class="d-flex mb-2">
<button
type="submit"
class="btn btn-primary btn-block btn-submit"
[disabled]="form.loading"
>
<span>{{ "submit" | i18n }}</span>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div> </div>

View File

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

View File

@@ -1,31 +1,55 @@
<div class="mt-5 d-flex justify-content-center" *ngIf="loading"> <div class="mt-5 d-flex justify-content-center" *ngIf="loading">
<div> <div>
<img class="mb-4 logo logo-themed" alt="Bitwarden"> <img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center"> <p class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i> <i
<span class="sr-only">{{'loading' | i18n}}</span> class="fa fa-spinner fa-spin fa-2x text-muted"
</p> title="{{ 'loading' | i18n }}"
</div> aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</div>
</div> </div>
<div class="container" *ngIf="!loading"> <div class="container" *ngIf="!loading">
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="lead text-center mb-4">{{'removeMasterPassword' | i18n}}</p> <p class="lead text-center mb-4">{{ "removeMasterPassword" | i18n }}</p>
<hr> <hr />
<div class="card d-block"> <div class="card d-block">
<div class="card-body"> <div class="card-body">
<p>{{'convertOrganizationEncryptionDesc' | i18n : organization.name}}</p> <p>{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}</p>
<button type="button" class="btn btn-primary btn-block" (click)="convert()" [disabled]="actionPromise"> <button
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true" *ngIf="continuing"></i> type="button"
{{'removeMasterPassword' | i18n}} class="btn btn-primary btn-block"
</button> (click)="convert()"
<button type="button" class="btn btn-outline-secondary btn-block" (click)="leave()" [disabled]="actionPromise"> [disabled]="actionPromise"
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true" *ngIf="leaving"></i> >
{{'leaveOrganization' | i18n}} <i
</button> class="fa fa-spinner fa-spin"
</div> title="{{ 'loading' | i18n }}"
</div> aria-hidden="true"
*ngIf="continuing"
></i>
{{ "removeMasterPassword" | i18n }}
</button>
<button
type="button"
class="btn btn-outline-secondary btn-block"
(click)="leave()"
[disabled]="actionPromise"
>
<i
class="fa fa-spinner fa-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
*ngIf="leaving"
></i>
{{ "leaveOrganization" | i18n }}
</button>
</div> </div>
</div>
</div> </div>
</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({ @Component({
selector: 'app-remove-password', selector: "app-remove-password",
templateUrl: 'remove-password.component.html', 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"> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="lead text-center mb-4">{{'setMasterPassword' | i18n}}</p> <p class="lead text-center mb-4">{{ "setMasterPassword" | i18n }}</p>
<div class="card d-block"> <div class="card d-block">
<div class="card-body text-center" *ngIf="syncLoading"> <div class="card-body text-center" *ngIf="syncLoading">
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i> <i class="fa fa-spinner fa-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{'loading' | i18n}} {{ "loading" | i18n }}
</div>
<div 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> </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> </div>
</div>
</form> </form>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,71 +1,86 @@
import { import { Component, ViewChild, ViewContainerRef } from "@angular/core";
Component,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { import { ActivatedRoute, Router } from "@angular/router";
ActivatedRoute,
Router,
} from '@angular/router';
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { EnvironmentService } from 'jslib-common/abstractions/environment.service'; import { EnvironmentService } from "jslib-common/abstractions/environment.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { ModalService } from 'jslib-angular/services/modal.service'; import { ModalService } from "jslib-angular/services/modal.service";
import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType'; import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib-angular/components/two-factor.component'; import { TwoFactorComponent as BaseTwoFactorComponent } from "jslib-angular/components/two-factor.component";
import { TwoFactorOptionsComponent } from './two-factor-options.component'; import { TwoFactorOptionsComponent } from "./two-factor-options.component";
@Component({ @Component({
selector: 'app-two-factor', selector: "app-two-factor",
templateUrl: 'two-factor.component.html', templateUrl: "two-factor.component.html",
}) })
export class TwoFactorComponent extends BaseTwoFactorComponent { export class TwoFactorComponent extends BaseTwoFactorComponent {
@ViewChild('twoFactorOptions', { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef; @ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true })
twoFactorOptionsModal: ViewContainerRef;
constructor(authService: AuthService, router: Router, constructor(
i18nService: I18nService, apiService: ApiService, authService: AuthService,
platformUtilsService: PlatformUtilsService, stateService: StateService, router: Router,
environmentService: EnvironmentService, private modalService: ModalService, i18nService: I18nService,
route: ActivatedRoute, logService: LogService) { apiService: ApiService,
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService, platformUtilsService: PlatformUtilsService,
stateService, route, logService); stateService: StateService,
this.onSuccessfulLoginNavigate = this.goAfterLogIn; environmentService: EnvironmentService,
} private modalService: ModalService,
route: ActivatedRoute,
logService: LogService
) {
super(
authService,
router,
i18nService,
apiService,
platformUtilsService,
window,
environmentService,
stateService,
route,
logService
);
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
}
async anotherMethod() { async anotherMethod() {
const [modal] = await this.modalService.openViewRef(TwoFactorOptionsComponent, this.twoFactorOptionsModal, comp => { const [modal] = await this.modalService.openViewRef(
comp.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => { TwoFactorOptionsComponent,
modal.close(); this.twoFactorOptionsModal,
this.selectedProviderType = provider; (comp) => {
await this.init(); comp.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => {
}); modal.close();
comp.onRecoverSelected.subscribe(() => { this.selectedProviderType = provider;
modal.close(); await this.init();
});
}); });
} comp.onRecoverSelected.subscribe(() => {
modal.close();
});
}
);
}
async goAfterLogIn() { async goAfterLogIn() {
const loginRedirect = await this.stateService.getLoginRedirect(); const loginRedirect = await this.stateService.getLoginRedirect();
if (loginRedirect != null) { if (loginRedirect != null) {
this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams }); this.router.navigate([loginRedirect.route], { queryParams: loginRedirect.qParams });
await this.stateService.setLoginRedirect(null); await this.stateService.setLoginRedirect(null);
} else { } else {
this.router.navigate([this.successRoute], { this.router.navigate([this.successRoute], {
queryParams: { queryParams: {
identifier: this.identifier, identifier: this.identifier,
}, },
}); });
}
} }
}
} }

View File

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

View File

@@ -1,29 +1,46 @@
import { Component } from '@angular/core'; import { Component } from "@angular/core";
import { ApiService } from 'jslib-common/abstractions/api.service'; import { ApiService } from "jslib-common/abstractions/api.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { MessagingService } from 'jslib-common/abstractions/messaging.service'; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from 'jslib-angular/components/update-temp-password.component'; import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "jslib-angular/components/update-temp-password.component";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
@Component({ @Component({
selector: 'app-update-temp-password', selector: "app-update-temp-password",
templateUrl: 'update-temp-password.component.html', templateUrl: "update-temp-password.component.html",
}) })
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent { export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
constructor(i18nService: I18nService, platformUtilsService: PlatformUtilsService, constructor(
passwordGenerationService: PasswordGenerationService, policyService: PolicyService, i18nService: I18nService,
cryptoService: CryptoService, messagingService: MessagingService, platformUtilsService: PlatformUtilsService,
apiService: ApiService, logService: LogService, stateService: StateService, syncService: SyncService) { passwordGenerationService: PasswordGenerationService,
super(i18nService, platformUtilsService, passwordGenerationService, policyService, cryptoService, policyService: PolicyService,
messagingService, apiService, stateService, syncService, logService); cryptoService: CryptoService,
} messagingService: MessagingService,
apiService: ApiService,
logService: LogService,
stateService: StateService,
syncService: SyncService
) {
super(
i18nService,
platformUtilsService,
passwordGenerationService,
policyService,
cryptoService,
messagingService,
apiService,
stateService,
syncService,
logService
);
}
} }

View File

@@ -1,9 +1,13 @@
<div class="mt-5 d-flex justify-content-center"> <div class="mt-5 d-flex justify-content-center">
<div> <div>
<img class="mb-4 logo logo-themed" alt="Bitwarden"> <img class="mb-4 logo logo-themed" alt="Bitwarden" />
<p class="text-center"> <p class="text-center">
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i> <i
<span class="sr-only">{{'loading' | i18n}}</span> class="fa fa-spinner fa-spin fa-2x text-muted"
</p> title="{{ 'loading' | i18n }}"
</div> aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</div>
</div> </div>

View File

@@ -1,55 +1,50 @@
import { import { Component, OnInit } from "@angular/core";
Component, import { ActivatedRoute, Router } from "@angular/router";
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 { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { VerifyEmailRequest } from 'jslib-common/models/request/verifyEmailRequest'; import { VerifyEmailRequest } from "jslib-common/models/request/verifyEmailRequest";
@Component({ @Component({
selector: 'app-verify-email-token', selector: "app-verify-email-token",
templateUrl: 'verify-email-token.component.html', templateUrl: "verify-email-token.component.html",
}) })
export class VerifyEmailTokenComponent implements OnInit { export class VerifyEmailTokenComponent implements OnInit {
constructor( constructor(
private router: Router, private router: Router,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private i18nService: I18nService,
private route: ActivatedRoute, private route: ActivatedRoute,
private apiService: ApiService, private apiService: ApiService,
private logService: LogService, private logService: LogService,
private stateService: StateService private stateService: StateService
) { } ) {}
ngOnInit() { ngOnInit() {
this.route.queryParams.pipe(first()).subscribe(async qParams => { this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.userId != null && qParams.token != null) { if (qParams.userId != null && qParams.token != null) {
try { try {
await this.apiService.postAccountVerifyEmailToken( await this.apiService.postAccountVerifyEmailToken(
new VerifyEmailRequest(qParams.userId, qParams.token)); new VerifyEmailRequest(qParams.userId, qParams.token)
if (await this.stateService.getIsAuthenticated()) { );
await this.apiService.refreshIdentityToken(); if (await this.stateService.getIsAuthenticated()) {
} await this.apiService.refreshIdentityToken();
this.platformUtilsService.showToast('success', null, this.i18nService.t('emailVerified')); }
this.router.navigate(['/']); this.platformUtilsService.showToast("success", null, this.i18nService.t("emailVerified"));
return; this.router.navigate(["/"]);
} catch (e) { return;
this.logService.error(e); } catch (e) {
} this.logService.error(e);
} }
this.platformUtilsService.showToast('error', null, this.i18nService.t('emailVerifiedFailed')); }
this.router.navigate(['/']); this.platformUtilsService.showToast("error", null, this.i18nService.t("emailVerifiedFailed"));
}); this.router.navigate(["/"]);
} });
}
} }

View File

@@ -1,26 +1,34 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate> <form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
<div class="row justify-content-md-center mt-5"> <div class="row justify-content-md-center mt-5">
<div class="col-5"> <div class="col-5">
<p class="lead text-center mb-4">{{'deleteAccount' | i18n}}</p> <p class="lead text-center mb-4">{{ "deleteAccount" | i18n }}</p>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<app-callout type="warning">{{'deleteAccountWarning' | i18n}}</app-callout> <app-callout type="warning">{{ "deleteAccountWarning" | i18n }}</app-callout>
<p class="text-center"> <p class="text-center">
<strong>{{email}}</strong> <strong>{{ email }}</strong>
</p> </p>
<p>{{'deleteRecoverConfirmDesc' | i18n}}</p> <p>{{ "deleteRecoverConfirmDesc" | i18n }}</p>
<hr> <hr />
<div class="d-flex"> <div class="d-flex">
<button type="submit" class="btn btn-danger btn-block btn-submit" [disabled]="form.loading"> <button
<span>{{'deleteAccount' | i18n}}</span> type="submit"
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i> class="btn btn-danger btn-block btn-submit"
</button> [disabled]="form.loading"
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0"> >
{{'cancel' | i18n}} <span>{{ "deleteAccount" | i18n }}</span>
</a> <i
</div> class="fa fa-spinner fa-spin"
</div> title="{{ 'loading' | i18n }}"
</div> aria-hidden="true"
></i>
</button>
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
{{ "cancel" | i18n }}
</a>
</div>
</div> </div>
</div>
</div> </div>
</div>
</form> </form>

View File

@@ -1,59 +1,60 @@
import { import { Component, OnInit } from "@angular/core";
Component, import { ActivatedRoute, Router } from "@angular/router";
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 { ApiService } from "jslib-common/abstractions/api.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from 'jslib-common/abstractions/log.service'; import { LogService } from "jslib-common/abstractions/log.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { VerifyDeleteRecoverRequest } from 'jslib-common/models/request/verifyDeleteRecoverRequest'; import { VerifyDeleteRecoverRequest } from "jslib-common/models/request/verifyDeleteRecoverRequest";
@Component({ @Component({
selector: 'app-verify-recover-delete', selector: "app-verify-recover-delete",
templateUrl: 'verify-recover-delete.component.html', templateUrl: "verify-recover-delete.component.html",
}) })
export class VerifyRecoverDeleteComponent implements OnInit { export class VerifyRecoverDeleteComponent implements OnInit {
email: string; email: string;
formPromise: Promise<any>; formPromise: Promise<any>;
private userId: string; private userId: string;
private token: string; private token: string;
constructor(private router: Router, private apiService: ApiService, constructor(
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private router: Router,
private route: ActivatedRoute, private logService: LogService) { private apiService: ApiService,
} private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
ngOnInit() { private route: ActivatedRoute,
this.route.queryParams.pipe(first()).subscribe(async qParams => { private logService: LogService
if (qParams.userId != null && qParams.token != null && qParams.email != null) { ) {}
this.userId = qParams.userId;
this.token = qParams.token; ngOnInit() {
this.email = qParams.email; this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
} else { if (qParams.userId != null && qParams.token != null && qParams.email != null) {
this.router.navigate(['/']); this.userId = qParams.userId;
} this.token = qParams.token;
}); this.email = qParams.email;
} } else {
this.router.navigate(["/"]);
async submit() { }
try { });
const request = new VerifyDeleteRecoverRequest(this.userId, this.token); }
this.formPromise = this.apiService.postAccountRecoverDeleteToken(request);
await this.formPromise; async submit() {
this.platformUtilsService.showToast('success', this.i18nService.t('accountDeleted'), try {
this.i18nService.t('accountDeletedDesc')); const request = new VerifyDeleteRecoverRequest(this.userId, this.token);
this.router.navigate(['/']); this.formPromise = this.apiService.postAccountRecoverDeleteToken(request);
} catch (e) { await this.formPromise;
this.logService.error(e); this.platformUtilsService.showToast(
} "success",
this.i18nService.t("accountDeleted"),
this.i18nService.t("accountDeletedDesc")
);
this.router.navigate(["/"]);
} catch (e) {
this.logService.error(e);
} }
}
} }

View File

@@ -1,300 +1,310 @@
import { import { Component, NgZone, OnDestroy, OnInit, SecurityContext } from "@angular/core";
Component, import { DomSanitizer } from "@angular/platform-browser";
NgZone, import { NavigationEnd, Router } from "@angular/router";
OnDestroy, import * as jq from "jquery";
OnInit, import { IndividualConfig, ToastrService } from "ngx-toastr";
SecurityContext, import Swal from "sweetalert2";
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import {
NavigationEnd,
Router,
} from '@angular/router';
import * as jq from 'jquery';
import { IndividualConfig, ToastrService } from 'ngx-toastr';
import Swal from 'sweetalert2';
import { AuthService } from 'jslib-common/abstractions/auth.service'; import { AuthService } from "jslib-common/abstractions/auth.service";
import { BroadcasterService } from 'jslib-common/abstractions/broadcaster.service'; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { CipherService } from 'jslib-common/abstractions/cipher.service'; import { CipherService } from "jslib-common/abstractions/cipher.service";
import { CollectionService } from 'jslib-common/abstractions/collection.service'; import { CollectionService } from "jslib-common/abstractions/collection.service";
import { CryptoService } from 'jslib-common/abstractions/crypto.service'; import { CryptoService } from "jslib-common/abstractions/crypto.service";
import { EventService } from 'jslib-common/abstractions/event.service'; import { EventService } from "jslib-common/abstractions/event.service";
import { FolderService } from 'jslib-common/abstractions/folder.service'; import { FolderService } from "jslib-common/abstractions/folder.service";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { KeyConnectorService } from 'jslib-common/abstractions/keyConnector.service'; import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { NotificationsService } from 'jslib-common/abstractions/notifications.service'; import { NotificationsService } from "jslib-common/abstractions/notifications.service";
import { PasswordGenerationService } from 'jslib-common/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from "jslib-common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { PolicyService } from 'jslib-common/abstractions/policy.service'; import { PolicyService } from "jslib-common/abstractions/policy.service";
import { SearchService } from 'jslib-common/abstractions/search.service'; import { SearchService } from "jslib-common/abstractions/search.service";
import { SettingsService } from 'jslib-common/abstractions/settings.service'; import { SettingsService } from "jslib-common/abstractions/settings.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
import { StorageService } from 'jslib-common/abstractions/storage.service'; import { StorageService } from "jslib-common/abstractions/storage.service";
import { SyncService } from 'jslib-common/abstractions/sync.service'; import { SyncService } from "jslib-common/abstractions/sync.service";
import { TokenService } from 'jslib-common/abstractions/token.service'; import { TokenService } from "jslib-common/abstractions/token.service";
import { VaultTimeoutService } from 'jslib-common/abstractions/vaultTimeout.service'; import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
import { PolicyListService } from './services/policy-list.service'; import { PolicyListService } from "./services/policy-list.service";
import { RouterService } from './services/router.service'; import { RouterService } from "./services/router.service";
import { DisableSendPolicy } from './organizations/policies/disable-send.component'; import { DisableSendPolicy } from "./organizations/policies/disable-send.component";
import { MasterPasswordPolicy } from './organizations/policies/master-password.component'; import { MasterPasswordPolicy } from "./organizations/policies/master-password.component";
import { PasswordGeneratorPolicy } from './organizations/policies/password-generator.component'; import { PasswordGeneratorPolicy } from "./organizations/policies/password-generator.component";
import { PersonalOwnershipPolicy } from './organizations/policies/personal-ownership.component'; import { PersonalOwnershipPolicy } from "./organizations/policies/personal-ownership.component";
import { RequireSsoPolicy } from './organizations/policies/require-sso.component'; import { RequireSsoPolicy } from "./organizations/policies/require-sso.component";
import { ResetPasswordPolicy } from './organizations/policies/reset-password.component'; import { ResetPasswordPolicy } from "./organizations/policies/reset-password.component";
import { SendOptionsPolicy } from './organizations/policies/send-options.component'; import { SendOptionsPolicy } from "./organizations/policies/send-options.component";
import { SingleOrgPolicy } from './organizations/policies/single-org.component'; import { SingleOrgPolicy } from "./organizations/policies/single-org.component";
import { TwoFactorAuthenticationPolicy } from './organizations/policies/two-factor-authentication.component'; import { TwoFactorAuthenticationPolicy } from "./organizations/policies/two-factor-authentication.component";
const BroadcasterSubscriptionId = 'AppComponent'; const BroadcasterSubscriptionId = "AppComponent";
const IdleTimeout = 60000 * 10; // 10 minutes const IdleTimeout = 60000 * 10; // 10 minutes
@Component({ @Component({
selector: 'app-root', selector: "app-root",
templateUrl: 'app.component.html', templateUrl: "app.component.html",
}) })
export class AppComponent implements OnDestroy, OnInit { export class AppComponent implements OnDestroy, OnInit {
private lastActivity: number = null;
private idleTimer: number = null;
private isIdle = false;
private lastActivity: number = null; constructor(
private idleTimer: number = null; private broadcasterService: BroadcasterService,
private isIdle = false; private tokenService: TokenService,
private folderService: FolderService,
private settingsService: SettingsService,
private syncService: SyncService,
private passwordGenerationService: PasswordGenerationService,
private cipherService: CipherService,
private authService: AuthService,
private router: Router,
private toastrService: ToastrService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private ngZone: NgZone,
private vaultTimeoutService: VaultTimeoutService,
private cryptoService: CryptoService,
private collectionService: CollectionService,
private sanitizer: DomSanitizer,
private searchService: SearchService,
private notificationsService: NotificationsService,
private routerService: RouterService,
private stateService: StateService,
private eventService: EventService,
private policyService: PolicyService,
protected policyListService: PolicyListService,
private keyConnectorService: KeyConnectorService
) {}
constructor( ngOnInit() {
private broadcasterService: BroadcasterService, this.ngZone.runOutsideAngular(() => {
private tokenService: TokenService, window.onmousemove = () => this.recordActivity();
private folderService: FolderService, window.onmousedown = () => this.recordActivity();
private settingsService: SettingsService, window.ontouchstart = () => this.recordActivity();
private syncService: SyncService, window.onclick = () => this.recordActivity();
private passwordGenerationService: PasswordGenerationService, window.onscroll = () => this.recordActivity();
private cipherService: CipherService, window.onkeypress = () => this.recordActivity();
private authService: AuthService, });
private router: Router,
private toastrService: ToastrService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private ngZone: NgZone,
private vaultTimeoutService: VaultTimeoutService,
private cryptoService: CryptoService,
private collectionService: CollectionService,
private sanitizer: DomSanitizer,
private searchService: SearchService,
private notificationsService: NotificationsService,
private routerService: RouterService,
private stateService: StateService,
private eventService: EventService,
private policyService: PolicyService,
protected policyListService: PolicyListService,
private keyConnectorService: KeyConnectorService
) { }
ngOnInit() { this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.runOutsideAngular(() => { this.ngZone.run(async () => {
window.onmousemove = () => this.recordActivity(); switch (message.command) {
window.onmousedown = () => this.recordActivity(); case "loggedIn":
window.ontouchstart = () => this.recordActivity(); case "loggedOut":
window.onclick = () => this.recordActivity(); case "unlocked":
window.onscroll = () => this.recordActivity(); this.notificationsService.updateConnection(false);
window.onkeypress = () => this.recordActivity(); break;
}); case "authBlocked":
this.router.navigate(["/"]);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { break;
this.ngZone.run(async () => { case "logout":
switch (message.command) { this.logOut(!!message.expired);
case 'loggedIn': break;
case 'loggedOut': case "lockVault":
case 'unlocked': await this.vaultTimeoutService.lock();
this.notificationsService.updateConnection(false); break;
break; case "locked":
case 'authBlocked': this.notificationsService.updateConnection(false);
this.router.navigate(['/']); this.router.navigate(["lock"]);
break; break;
case 'logout': case "lockedUrl":
this.logOut(!!message.expired); window.setTimeout(() => this.routerService.setPreviousUrl(message.url), 500);
break; break;
case 'lockVault': case "syncStarted":
await this.vaultTimeoutService.lock(); break;
break; case "syncCompleted":
case 'locked': break;
this.notificationsService.updateConnection(false); case "upgradeOrganization":
this.router.navigate(['lock']); const upgradeConfirmed = await this.platformUtilsService.showDialog(
break; this.i18nService.t("upgradeOrganizationDesc"),
case 'lockedUrl': this.i18nService.t("upgradeOrganization"),
window.setTimeout(() => this.routerService.setPreviousUrl(message.url), 500); this.i18nService.t("upgradeOrganization"),
break; this.i18nService.t("cancel")
case 'syncStarted': );
break; if (upgradeConfirmed) {
case 'syncCompleted': this.router.navigate([
break; "organizations",
case 'upgradeOrganization': message.organizationId,
const upgradeConfirmed = await this.platformUtilsService.showDialog( "settings",
this.i18nService.t('upgradeOrganizationDesc'), this.i18nService.t('upgradeOrganization'), "billing",
this.i18nService.t('upgradeOrganization'), this.i18nService.t('cancel')); ]);
if (upgradeConfirmed) {
this.router.navigate(['organizations', message.organizationId, 'settings', 'billing']);
}
break;
case 'premiumRequired':
const premiumConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('premiumRequiredDesc'), this.i18nService.t('premiumRequired'),
this.i18nService.t('learnMore'), this.i18nService.t('cancel'));
if (premiumConfirmed) {
this.router.navigate(['settings/premium']);
}
break;
case 'emailVerificationRequired':
const emailVerificationConfirmed = await this.platformUtilsService.showDialog(
this.i18nService.t('emailVerificationRequiredDesc'),
this.i18nService.t('emailVerificationRequired'),
this.i18nService.t('learnMore'), this.i18nService.t('cancel'));
if (emailVerificationConfirmed) {
this.platformUtilsService.launchUri('https://bitwarden.com/help/article/create-bitwarden-account/');
}
break;
case 'showToast':
this.showToast(message);
break;
case 'setFullWidth':
this.setFullWidth();
break;
case 'convertAccountToKeyConnector':
this.keyConnectorService.setConvertAccountRequired(true);
this.router.navigate(['/remove-password']);
break;
default:
break;
}
});
});
this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) {
const modals = Array.from(document.querySelectorAll('.modal'));
for (const modal of modals) {
(jq(modal) as any).modal('hide');
}
if (document.querySelector('.swal-modal') != null) {
Swal.close(undefined);
}
} }
}); break;
case "premiumRequired":
this.policyListService.addPolicies([ const premiumConfirmed = await this.platformUtilsService.showDialog(
new TwoFactorAuthenticationPolicy(), this.i18nService.t("premiumRequiredDesc"),
new MasterPasswordPolicy(), this.i18nService.t("premiumRequired"),
new PasswordGeneratorPolicy(), this.i18nService.t("learnMore"),
new SingleOrgPolicy(), this.i18nService.t("cancel")
new RequireSsoPolicy(), );
new PersonalOwnershipPolicy(), if (premiumConfirmed) {
new DisableSendPolicy(), this.router.navigate(["settings/premium"]);
new SendOptionsPolicy(),
new ResetPasswordPolicy(),
]);
this.setFullWidth();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
private async logOut(expired: boolean) {
await this.eventService.uploadEvents();
const userId = await this.stateService.getUserId();
await Promise.all([
this.eventService.clearEvents(),
this.syncService.setLastSync(new Date(0)),
this.tokenService.clearToken(),
this.cryptoService.clearKeys(),
this.settingsService.clear(userId),
this.cipherService.clear(userId),
this.folderService.clear(userId),
this.collectionService.clear(userId),
this.policyService.clear(userId),
this.passwordGenerationService.clear(),
this.keyConnectorService.clear(),
]);
this.searchService.clearIndex();
this.authService.logOut(async () => {
if (expired) {
this.platformUtilsService.showToast('warning', this.i18nService.t('loggedOut'),
this.i18nService.t('loginExpired'));
} }
break;
await this.stateService.clean({ userId: userId }); case "emailVerificationRequired":
Swal.close(); const emailVerificationConfirmed = await this.platformUtilsService.showDialog(
this.router.navigate(['/']); this.i18nService.t("emailVerificationRequiredDesc"),
}); this.i18nService.t("emailVerificationRequired"),
} this.i18nService.t("learnMore"),
this.i18nService.t("cancel")
private async recordActivity() { );
const now = (new Date()).getTime(); if (emailVerificationConfirmed) {
if (this.lastActivity != null && now - this.lastActivity < 250) { this.platformUtilsService.launchUri(
return; "https://bitwarden.com/help/article/create-bitwarden-account/"
} );
this.lastActivity = now;
this.stateService.setLastActive(now);
// Idle states
if (this.isIdle) {
this.isIdle = false;
this.idleStateChanged();
}
if (this.idleTimer != null) {
window.clearTimeout(this.idleTimer);
this.idleTimer = null;
}
this.idleTimer = window.setTimeout(() => {
if (!this.isIdle) {
this.isIdle = true;
this.idleStateChanged();
} }
}, IdleTimeout); break;
} case "showToast":
this.showToast(message);
private showToast(msg: any) { break;
let message = ''; case "setFullWidth":
this.setFullWidth();
const options: Partial<IndividualConfig> = {}; break;
case "convertAccountToKeyConnector":
if (typeof (msg.text) === 'string') { this.keyConnectorService.setConvertAccountRequired(true);
message = msg.text; this.router.navigate(["/remove-password"]);
} else if (msg.text.length === 1) { break;
message = msg.text[0]; default:
} else { break;
msg.text.forEach((t: string) =>
message += ('<p>' + this.sanitizer.sanitize(SecurityContext.HTML, t) + '</p>'));
options.enableHtml = true;
} }
if (msg.options != null) { });
if (msg.options.trustedHtml === true) { });
options.enableHtml = true;
} this.router.events.subscribe((event) => {
if (msg.options.timeout != null && msg.options.timeout > 0) { if (event instanceof NavigationEnd) {
options.timeOut = msg.options.timeout; const modals = Array.from(document.querySelectorAll(".modal"));
} for (const modal of modals) {
(jq(modal) as any).modal("hide");
} }
this.toastrService.show(message, msg.title, options, 'toast-' + msg.type); if (document.querySelector(".swal-modal") != null) {
Swal.close(undefined);
}
}
});
this.policyListService.addPolicies([
new TwoFactorAuthenticationPolicy(),
new MasterPasswordPolicy(),
new PasswordGeneratorPolicy(),
new SingleOrgPolicy(),
new RequireSsoPolicy(),
new PersonalOwnershipPolicy(),
new DisableSendPolicy(),
new SendOptionsPolicy(),
new ResetPasswordPolicy(),
]);
this.setFullWidth();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
private async logOut(expired: boolean) {
await this.eventService.uploadEvents();
const userId = await this.stateService.getUserId();
await Promise.all([
this.eventService.clearEvents(),
this.syncService.setLastSync(new Date(0)),
this.tokenService.clearToken(),
this.cryptoService.clearKeys(),
this.settingsService.clear(userId),
this.cipherService.clear(userId),
this.folderService.clear(userId),
this.collectionService.clear(userId),
this.policyService.clear(userId),
this.passwordGenerationService.clear(),
this.keyConnectorService.clear(),
]);
this.searchService.clearIndex();
this.authService.logOut(async () => {
if (expired) {
this.platformUtilsService.showToast(
"warning",
this.i18nService.t("loggedOut"),
this.i18nService.t("loginExpired")
);
}
await this.stateService.clean({ userId: userId });
Swal.close();
this.router.navigate(["/"]);
});
}
private async recordActivity() {
const now = new Date().getTime();
if (this.lastActivity != null && now - this.lastActivity < 250) {
return;
} }
private idleStateChanged() { this.lastActivity = now;
if (this.isIdle) { this.stateService.setLastActive(now);
this.notificationsService.disconnectFromInactivity(); // Idle states
} else { if (this.isIdle) {
this.notificationsService.reconnectFromActivity(); this.isIdle = false;
} this.idleStateChanged();
}
if (this.idleTimer != null) {
window.clearTimeout(this.idleTimer);
this.idleTimer = null;
}
this.idleTimer = window.setTimeout(() => {
if (!this.isIdle) {
this.isIdle = true;
this.idleStateChanged();
}
}, IdleTimeout);
}
private showToast(msg: any) {
let message = "";
const options: Partial<IndividualConfig> = {};
if (typeof msg.text === "string") {
message = msg.text;
} else if (msg.text.length === 1) {
message = msg.text[0];
} else {
msg.text.forEach(
(t: string) =>
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>")
);
options.enableHtml = true;
}
if (msg.options != null) {
if (msg.options.trustedHtml === true) {
options.enableHtml = true;
}
if (msg.options.timeout != null && msg.options.timeout > 0) {
options.timeOut = msg.options.timeout;
}
} }
private async setFullWidth() { this.toastrService.show(message, msg.title, options, "toast-" + msg.type);
const enableFullWidth = await this.stateService.getEnableFullWidth(); }
if (enableFullWidth) {
document.body.classList.add('full-width'); private idleStateChanged() {
} else { if (this.isIdle) {
document.body.classList.remove('full-width'); this.notificationsService.disconnectFromInactivity();
} } else {
this.notificationsService.reconnectFromActivity();
} }
}
private async setFullWidth() {
const enableFullWidth = await this.stateService.getEnableFullWidth();
if (enableFullWidth) {
document.body.classList.add("full-width");
} else {
document.body.classList.remove("full-width");
}
}
} }

View File

@@ -1,37 +1,35 @@
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from "ngx-infinite-scroll";
import { DragDropModule } from '@angular/cdk/drag-drop'; import { DragDropModule } from "@angular/cdk/drag-drop";
import { NgModule } from '@angular/core'; import { NgModule } from "@angular/core";
import { FormsModule } from '@angular/forms'; import { FormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { BitwardenToastModule } from 'jslib-angular/components/toastr.component'; import { BitwardenToastModule } from "jslib-angular/components/toastr.component";
import { AppComponent } from './app.component'; import { AppComponent } from "./app.component";
import { OssRoutingModule } from './oss-routing.module'; import { OssRoutingModule } from "./oss-routing.module";
import { OssModule } from './oss.module'; import { OssModule } from "./oss.module";
import { ServicesModule } from './services/services.module'; import { ServicesModule } from "./services/services.module";
import { WildcardRoutingModule } from './wildcard-routing.module'; import { WildcardRoutingModule } from "./wildcard-routing.module";
@NgModule({ @NgModule({
imports: [ imports: [
OssModule, OssModule,
BrowserAnimationsModule, BrowserAnimationsModule,
FormsModule, FormsModule,
ServicesModule, ServicesModule,
BitwardenToastModule.forRoot({ BitwardenToastModule.forRoot({
maxOpened: 5, maxOpened: 5,
autoDismiss: true, autoDismiss: true,
closeButton: true, closeButton: true,
}), }),
InfiniteScrollModule, InfiniteScrollModule,
DragDropModule, DragDropModule,
OssRoutingModule, OssRoutingModule,
WildcardRoutingModule, // Needs to be last to catch all non-existing routes WildcardRoutingModule, // Needs to be last to catch all non-existing routes
], ],
declarations: [ declarations: [AppComponent],
AppComponent, bootstrap: [AppComponent],
],
bootstrap: [AppComponent],
}) })
export class AppModule { } export class AppModule {}

View File

@@ -1,76 +1,78 @@
import { import { Directive, OnInit } from "@angular/core";
Directive, import { ActivatedRoute, Router } from "@angular/router";
OnInit,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { first } from 'rxjs/operators'; import { first } from "rxjs/operators";
import { I18nService } from 'jslib-common/abstractions/i18n.service'; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { StateService } from 'jslib-common/abstractions/state.service'; import { StateService } from "jslib-common/abstractions/state.service";
@Directive() @Directive()
export abstract class BaseAcceptComponent implements OnInit { export abstract class BaseAcceptComponent implements OnInit {
loading = true; loading = true;
authed = false; authed = false;
email: string; email: string;
actionPromise: Promise<any>; actionPromise: Promise<any>;
protected requiredParameters: string[] = []; protected requiredParameters: string[] = [];
protected failedShortMessage = 'inviteAcceptFailedShort'; protected failedShortMessage = "inviteAcceptFailedShort";
protected failedMessage = 'inviteAcceptFailed'; protected failedMessage = "inviteAcceptFailed";
constructor(protected router: Router, protected platformUtilService: PlatformUtilsService, constructor(
protected i18nService: I18nService, protected route: ActivatedRoute, protected router: Router,
protected stateService: StateService) { } protected platformUtilService: PlatformUtilsService,
protected i18nService: I18nService,
protected route: ActivatedRoute,
protected stateService: StateService
) {}
abstract authedHandler(qParams: any): Promise<void>; abstract authedHandler(qParams: any): Promise<void>;
abstract unauthedHandler(qParams: any): Promise<void>; abstract unauthedHandler(qParams: any): Promise<void>;
ngOnInit() { ngOnInit() {
this.route.queryParams.pipe(first()).subscribe(async qParams => { this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
await this.stateService.setLoginRedirect(null); await this.stateService.setLoginRedirect(null);
let error = this.requiredParameters.some(e => qParams?.[e] == null || qParams[e] === ''); let error = this.requiredParameters.some((e) => qParams?.[e] == null || qParams[e] === "");
let errorMessage: string = null; let errorMessage: string = null;
if (!error) { if (!error) {
this.authed = await this.stateService.getIsAuthenticated(); this.authed = await this.stateService.getIsAuthenticated();
if (this.authed) { if (this.authed) {
try { try {
await this.authedHandler(qParams); await this.authedHandler(qParams);
} catch (e) { } catch (e) {
error = true; error = true;
errorMessage = e.message; errorMessage = e.message;
} }
} else { } else {
await this.stateService.setLoginRedirect({ await this.stateService.setLoginRedirect({
route: this.getRedirectRoute(), route: this.getRedirectRoute(),
qParams: qParams, qParams: qParams,
}); });
this.email = qParams.email; this.email = qParams.email;
await this.unauthedHandler(qParams); await this.unauthedHandler(qParams);
} }
} }
if (error) { if (error) {
const message = errorMessage != null ? this.i18nService.t(this.failedShortMessage, errorMessage) : const message =
this.i18nService.t(this.failedMessage); errorMessage != null
this.platformUtilService.showToast('error', null, message, {timeout: 10000}); ? this.i18nService.t(this.failedShortMessage, errorMessage)
this.router.navigate(['/']); : this.i18nService.t(this.failedMessage);
} this.platformUtilService.showToast("error", null, message, {
timeout: 10000,
this.loading = false;
}); });
} this.router.navigate(["/"]);
}
getRedirectRoute() { this.loading = false;
const urlTree = this.router.parseUrl(this.router.url); });
urlTree.queryParams = {}; }
return urlTree.toString();
} getRedirectRoute() {
const urlTree = this.router.parseUrl(this.router.url);
urlTree.queryParams = {};
return urlTree.toString();
}
} }

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