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

@@ -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

@@ -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>
{{ "ssoPolicyHelpStart" | i18n }}
<a routerLink="../policies">{{ "ssoPolicyHelpLink" | i18n }}</a>
{{ "ssoPolicyHelpEnd" | i18n }}
<br />
{{ "ssoPolicyHelpKeyConnector" | i18n }}
</p>
<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">{{'allowSso' | i18n}}</label> class="form-check-input"
</div> type="checkbox"
<small class="form-text text-muted">{{'allowSsoDesc' | i18n}}</small> 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>

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();
}
} }

View File

@@ -1,156 +1,193 @@
import { Directive } from '@angular/core'; import { Directive } from "@angular/core";
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 { EventView } from 'jslib-common/models/view/eventView'; import { EventView } from "jslib-common/models/view/eventView";
import { EventResponse } from 'jslib-common/models/response/eventResponse'; import { EventResponse } from "jslib-common/models/response/eventResponse";
import { ListResponse } from 'jslib-common/models/response/listResponse'; import { ListResponse } from "jslib-common/models/response/listResponse";
import { EventService } from 'src/app/services/event.service'; import { EventService } from "src/app/services/event.service";
@Directive() @Directive()
export abstract class BaseEventsComponent { export abstract class BaseEventsComponent {
loading = true; loading = true;
loaded = false; loaded = false;
events: EventView[]; events: EventView[];
start: string; start: string;
end: string; end: string;
dirtyDates: boolean = true; dirtyDates: boolean = true;
continuationToken: string; continuationToken: string;
refreshPromise: Promise<any>; refreshPromise: Promise<any>;
exportPromise: Promise<any>; exportPromise: Promise<any>;
morePromise: Promise<any>; morePromise: Promise<any>;
abstract readonly exportFileName: string; abstract readonly exportFileName: string;
constructor(protected eventService: EventService, protected i18nService: I18nService, constructor(
protected exportService: ExportService, protected platformUtilsService: PlatformUtilsService, protected eventService: EventService,
protected logService: LogService) { protected i18nService: I18nService,
const defaultDates = this.eventService.getDefaultDateFilters(); protected exportService: ExportService,
this.start = defaultDates[0]; protected platformUtilsService: PlatformUtilsService,
this.end = defaultDates[1]; protected logService: LogService
) {
const defaultDates = this.eventService.getDefaultDateFilters();
this.start = defaultDates[0];
this.end = defaultDates[1];
}
async exportEvents() {
if (this.appApiPromiseUnfulfilled() || this.dirtyDates) {
return;
} }
async exportEvents() { this.loading = true;
if (this.appApiPromiseUnfulfilled() || this.dirtyDates) {
return;
}
this.loading = true; const dates = this.parseDates();
if (dates == null) {
const dates = this.parseDates(); return;
if (dates == null) {
return;
}
try {
this.exportPromise = this.export(dates[0], dates[1]);
await this.exportPromise;
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
this.exportPromise = null;
this.loading = false;
} }
async loadEvents(clearExisting: boolean) { try {
if (this.appApiPromiseUnfulfilled()) { this.exportPromise = this.export(dates[0], dates[1]);
return;
}
const dates = this.parseDates(); await this.exportPromise;
if (dates == null) { } catch (e) {
return; this.logService.error(`Handled exception: ${e}`);
}
this.loading = true;
let events: EventView[] = [];
try {
const promise = this.loadAndParseEvents(dates[0], dates[1], clearExisting ? null : this.continuationToken);
if (clearExisting) {
this.refreshPromise = promise;
} else {
this.morePromise = promise;
}
const result = await promise;
this.continuationToken = result.continuationToken;
events = result.events;
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
}
if (!clearExisting && this.events != null && this.events.length > 0) {
this.events = this.events.concat(events);
} else {
this.events = events;
}
this.dirtyDates = false;
this.loading = false;
this.morePromise = null;
this.refreshPromise = null;
} }
protected abstract requestEvents(startDate: string, endDate: string, continuationToken: string): Promise<ListResponse<EventResponse>>; this.exportPromise = null;
protected abstract getUserName(r: EventResponse, userId: string): { name: string, email: string }; this.loading = false;
}
protected async loadAndParseEvents(startDate: string, endDate: string, continuationToken: string) { async loadEvents(clearExisting: boolean) {
const response = await this.requestEvents(startDate, endDate, continuationToken); if (this.appApiPromiseUnfulfilled()) {
return;
const events = await Promise.all(response.data.map(async r => {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = await this.eventService.getEventInfo(r);
const user = this.getUserName(r, userId);
return new EventView({
message: eventInfo.message,
humanReadableMessage: eventInfo.humanReadableMessage,
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
userId: userId,
userName: user != null ? user.name : this.i18nService.t('unknown'),
userEmail: user != null ? user.email : '',
date: r.date,
ip: r.ipAddress,
type: r.type,
});
}));
return { continuationToken: response.continuationToken, events: events };
} }
protected parseDates() { const dates = this.parseDates();
let dates: string[] = null; if (dates == null) {
try { return;
dates = this.eventService.formatDateFilters(this.start, this.end);
} catch (e) {
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
this.i18nService.t('invalidDateRange'));
return null;
}
return dates;
} }
protected appApiPromiseUnfulfilled() { this.loading = true;
return this.refreshPromise != null || this.morePromise != null || this.exportPromise != null; let events: EventView[] = [];
try {
const promise = this.loadAndParseEvents(
dates[0],
dates[1],
clearExisting ? null : this.continuationToken
);
if (clearExisting) {
this.refreshPromise = promise;
} else {
this.morePromise = promise;
}
const result = await promise;
this.continuationToken = result.continuationToken;
events = result.events;
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
} }
private async export(start: string, end: string) { if (!clearExisting && this.events != null && this.events.length > 0) {
let continuationToken = this.continuationToken; this.events = this.events.concat(events);
let events = [].concat(this.events); } else {
this.events = events;
while (continuationToken != null) {
const result = await this.loadAndParseEvents(start, end, continuationToken);
continuationToken = result.continuationToken;
events = events.concat(result.events);
}
const data = await this.exportService.getEventExport(events);
const fileName = this.exportService.getFileName(this.exportFileName, 'csv');
this.platformUtilsService.saveFile(window, data, { type: 'text/plain' }, fileName);
} }
this.dirtyDates = false;
this.loading = false;
this.morePromise = null;
this.refreshPromise = null;
}
protected abstract requestEvents(
startDate: string,
endDate: string,
continuationToken: string
): Promise<ListResponse<EventResponse>>;
protected abstract getUserName(
r: EventResponse,
userId: string
): {
name: string;
email: string;
};
protected async loadAndParseEvents(
startDate: string,
endDate: string,
continuationToken: string
) {
const response = await this.requestEvents(startDate, endDate, continuationToken);
const events = await Promise.all(
response.data.map(async (r) => {
const userId = r.actingUserId == null ? r.userId : r.actingUserId;
const eventInfo = await this.eventService.getEventInfo(r);
const user = this.getUserName(r, userId);
return new EventView({
message: eventInfo.message,
humanReadableMessage: eventInfo.humanReadableMessage,
appIcon: eventInfo.appIcon,
appName: eventInfo.appName,
userId: userId,
userName: user != null ? user.name : this.i18nService.t("unknown"),
userEmail: user != null ? user.email : "",
date: r.date,
ip: r.ipAddress,
type: r.type,
});
})
);
return {
continuationToken: response.continuationToken,
events: events,
};
}
protected parseDates() {
let dates: string[] = null;
try {
dates = this.eventService.formatDateFilters(this.start, this.end);
} catch (e) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("invalidDateRange")
);
return null;
}
return dates;
}
protected appApiPromiseUnfulfilled() {
return this.refreshPromise != null || this.morePromise != null || this.exportPromise != null;
}
private async export(start: string, end: string) {
let continuationToken = this.continuationToken;
let events = [].concat(this.events);
while (continuationToken != null) {
const result = await this.loadAndParseEvents(start, end, continuationToken);
continuationToken = result.continuationToken;
events = events.concat(result.events);
}
const data = await this.exportService.getEventExport(events);
const fileName = this.exportService.getFileName(this.exportFileName, "csv");
this.platformUtilsService.saveFile(
window,
data,
{
type: "text/plain",
},
fileName
);
}
} }

View File

@@ -1,320 +1,353 @@
import { import { Directive, ViewChild, ViewContainerRef } from "@angular/core";
Directive,
ViewChild,
ViewContainerRef
} 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 { PlatformUtilsService } from 'jslib-common/abstractions/platformUtils.service'; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.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 { 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 { OrganizationUserStatusType } from 'jslib-common/enums/organizationUserStatusType'; import { OrganizationUserStatusType } from "jslib-common/enums/organizationUserStatusType";
import { OrganizationUserType } from 'jslib-common/enums/organizationUserType'; import { OrganizationUserType } from "jslib-common/enums/organizationUserType";
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 { ListResponse } from 'jslib-common/models/response/listResponse'; import { ListResponse } from "jslib-common/models/response/listResponse";
import { OrganizationUserUserDetailsResponse } from 'jslib-common/models/response/organizationUserResponse'; import { OrganizationUserUserDetailsResponse } from "jslib-common/models/response/organizationUserResponse";
import { ProviderUserUserDetailsResponse } from 'jslib-common/models/response/provider/providerUserResponse'; import { ProviderUserUserDetailsResponse } from "jslib-common/models/response/provider/providerUserResponse";
import { Utils } from 'jslib-common/misc/utils'; import { Utils } from "jslib-common/misc/utils";
import { UserConfirmComponent } from '../organizations/manage/user-confirm.component'; import { UserConfirmComponent } from "../organizations/manage/user-confirm.component";
type StatusType = OrganizationUserStatusType | ProviderUserStatusType; type StatusType = OrganizationUserStatusType | ProviderUserStatusType;
const MaxCheckedCount = 500; const MaxCheckedCount = 500;
@Directive() @Directive()
export abstract class BasePeopleComponent<UserType extends ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse> { export abstract class BasePeopleComponent<
UserType extends ProviderUserUserDetailsResponse | OrganizationUserUserDetailsResponse
> {
@ViewChild("confirmTemplate", {
read: ViewContainerRef,
static: true,
})
confirmModalRef: ViewContainerRef;
@ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef; get allCount() {
return this.allUsers != null ? this.allUsers.length : 0;
}
get allCount() { get invitedCount() {
return this.allUsers != null ? this.allUsers.length : 0; return this.statusMap.has(this.userStatusType.Invited)
? this.statusMap.get(this.userStatusType.Invited).length
: 0;
}
get acceptedCount() {
return this.statusMap.has(this.userStatusType.Accepted)
? this.statusMap.get(this.userStatusType.Accepted).length
: 0;
}
get confirmedCount() {
return this.statusMap.has(this.userStatusType.Confirmed)
? this.statusMap.get(this.userStatusType.Confirmed).length
: 0;
}
get showConfirmUsers(): boolean {
return (
this.allUsers != null &&
this.statusMap != null &&
this.allUsers.length > 1 &&
this.confirmedCount > 0 &&
this.confirmedCount < 3 &&
this.acceptedCount > 0
);
}
get showBulkConfirmUsers(): boolean {
return this.acceptedCount > 0;
}
abstract userType: typeof OrganizationUserType | typeof ProviderUserType;
abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType;
loading = true;
statusMap = new Map<StatusType, UserType[]>();
status: StatusType;
users: UserType[] = [];
pagedUsers: UserType[] = [];
searchText: string;
actionPromise: Promise<any>;
protected allUsers: UserType[] = [];
protected didScroll = false;
protected pageSize = 100;
private pagedUsersCount = 0;
constructor(
protected apiService: ApiService,
private searchService: SearchService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected cryptoService: CryptoService,
protected validationService: ValidationService,
protected modalService: ModalService,
private logService: LogService,
private searchPipe: SearchPipe,
protected userNamePipe: UserNamePipe,
protected stateService: StateService
) {}
abstract edit(user: UserType): void;
abstract getUsers(): Promise<ListResponse<UserType>>;
abstract deleteUser(id: string): Promise<any>;
abstract reinviteUser(id: string): Promise<any>;
abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<any>;
async load() {
const response = await this.getUsers();
this.statusMap.clear();
for (const status of Utils.iterateEnum(this.userStatusType)) {
this.statusMap.set(status, []);
} }
get invitedCount() { this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
return this.statusMap.has(this.userStatusType.Invited) ? this.allUsers.sort(Utils.getSortFunction(this.i18nService, "email"));
this.statusMap.get(this.userStatusType.Invited).length : 0; this.allUsers.forEach((u) => {
if (!this.statusMap.has(u.status)) {
this.statusMap.set(u.status, [u]);
} else {
this.statusMap.get(u.status).push(u);
}
});
this.filter(this.status);
this.loading = false;
}
filter(status: StatusType) {
this.status = status;
if (this.status != null) {
this.users = this.statusMap.get(this.status);
} else {
this.users = this.allUsers;
}
// Reset checkbox selecton
this.selectAll(false);
this.resetPaging();
}
loadMore() {
if (!this.users || this.users.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedUsers.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedUsersCount > this.pageSize) {
pagedSize = this.pagedUsersCount;
}
if (this.users.length > pagedLength) {
this.pagedUsers = this.pagedUsers.concat(
this.users.slice(pagedLength, pagedLength + pagedSize)
);
}
this.pagedUsersCount = this.pagedUsers.length;
this.didScroll = this.pagedUsers.length > this.pageSize;
}
checkUser(user: OrganizationUserUserDetailsResponse, select?: boolean) {
(user as any).checked = select == null ? !(user as any).checked : select;
}
selectAll(select: boolean) {
if (select) {
this.selectAll(false);
} }
get acceptedCount() { const filteredUsers = this.searchPipe.transform(
return this.statusMap.has(this.userStatusType.Accepted) ? this.users,
this.statusMap.get(this.userStatusType.Accepted).length : 0; this.searchText,
"name",
"email",
"id"
);
const selectCount =
select && filteredUsers.length > MaxCheckedCount ? MaxCheckedCount : filteredUsers.length;
for (let i = 0; i < selectCount; i++) {
this.checkUser(filteredUsers[i], select);
}
}
async resetPaging() {
this.pagedUsers = [];
this.loadMore();
}
invite() {
this.edit(null);
}
async remove(user: UserType) {
const confirmed = await this.platformUtilsService.showDialog(
this.deleteWarningMessage(user),
this.userNamePipe.transform(user),
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return false;
} }
get confirmedCount() { this.actionPromise = this.deleteUser(user.id);
return this.statusMap.has(this.userStatusType.Confirmed) ? try {
this.statusMap.get(this.userStatusType.Confirmed).length : 0; await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("removedUserId", this.userNamePipe.transform(user))
);
this.removeUser(user);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async reinvite(user: UserType) {
if (this.actionPromise != null) {
return;
} }
get showConfirmUsers(): boolean { this.actionPromise = this.reinviteUser(user.id);
return this.allUsers != null && this.statusMap != null && this.allUsers.length > 1 && try {
this.confirmedCount > 0 && this.confirmedCount < 3 && this.acceptedCount > 0; await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user))
);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null;
}
async confirm(user: UserType) {
function updateUser(self: BasePeopleComponent<UserType>) {
user.status = self.userStatusType.Confirmed;
const mapIndex = self.statusMap.get(self.userStatusType.Accepted).indexOf(user);
if (mapIndex > -1) {
self.statusMap.get(self.userStatusType.Accepted).splice(mapIndex, 1);
self.statusMap.get(self.userStatusType.Confirmed).push(user);
}
} }
get showBulkConfirmUsers(): boolean { const confirmUser = async (publicKey: Uint8Array) => {
return this.acceptedCount > 0; try {
} this.actionPromise = this.confirmUser(user, publicKey);
await this.actionPromise;
abstract userType: typeof OrganizationUserType | typeof ProviderUserType; updateUser(this);
abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType; this.platformUtilsService.showToast(
"success",
loading = true; null,
statusMap = new Map<StatusType, UserType[]>(); this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user))
status: StatusType; );
users: UserType[] = []; } catch (e) {
pagedUsers: UserType[] = []; this.validationService.showError(e);
searchText: string; throw e;
actionPromise: Promise<any>; } finally {
protected allUsers: UserType[] = [];
protected didScroll = false;
protected pageSize = 100;
private pagedUsersCount = 0;
constructor(
protected apiService: ApiService,
private searchService: SearchService,
protected i18nService: I18nService,
protected platformUtilsService: PlatformUtilsService,
protected cryptoService: CryptoService,
protected validationService: ValidationService,
protected modalService: ModalService,
private logService: LogService,
private searchPipe: SearchPipe,
protected userNamePipe: UserNamePipe,
protected stateService: StateService,
) { }
abstract edit(user: UserType): void;
abstract getUsers(): Promise<ListResponse<UserType>>;
abstract deleteUser(id: string): Promise<any>;
abstract reinviteUser(id: string): Promise<any>;
abstract confirmUser(user: UserType, publicKey: Uint8Array): Promise<any>;
async load() {
const response = await this.getUsers();
this.statusMap.clear();
for (const status of Utils.iterateEnum(this.userStatusType)) {
this.statusMap.set(status, []);
}
this.allUsers = response.data != null && response.data.length > 0 ? response.data : [];
this.allUsers.sort(Utils.getSortFunction(this.i18nService, 'email'));
this.allUsers.forEach(u => {
if (!this.statusMap.has(u.status)) {
this.statusMap.set(u.status, [u]);
} else {
this.statusMap.get(u.status).push(u);
}
});
this.filter(this.status);
this.loading = false;
}
filter(status: StatusType) {
this.status = status;
if (this.status != null) {
this.users = this.statusMap.get(this.status);
} else {
this.users = this.allUsers;
}
// Reset checkbox selecton
this.selectAll(false);
this.resetPaging();
}
loadMore() {
if (!this.users || this.users.length <= this.pageSize) {
return;
}
const pagedLength = this.pagedUsers.length;
let pagedSize = this.pageSize;
if (pagedLength === 0 && this.pagedUsersCount > this.pageSize) {
pagedSize = this.pagedUsersCount;
}
if (this.users.length > pagedLength) {
this.pagedUsers = this.pagedUsers.concat(this.users.slice(pagedLength, pagedLength + pagedSize));
}
this.pagedUsersCount = this.pagedUsers.length;
this.didScroll = this.pagedUsers.length > this.pageSize;
}
checkUser(user: OrganizationUserUserDetailsResponse, select?: boolean) {
(user as any).checked = select == null ? !(user as any).checked : select;
}
selectAll(select: boolean) {
if (select) {
this.selectAll(false);
}
const filteredUsers = this.searchPipe.transform(this.users, this.searchText, 'name', 'email', 'id');
const selectCount = select && filteredUsers.length > MaxCheckedCount
? MaxCheckedCount
: filteredUsers.length;
for (let i = 0; i < selectCount; i++) {
this.checkUser(filteredUsers[i], select);
}
}
async resetPaging() {
this.pagedUsers = [];
this.loadMore();
}
invite() {
this.edit(null);
}
async remove(user: UserType) {
const confirmed = await this.platformUtilsService.showDialog(
this.deleteWarningMessage(user), this.userNamePipe.transform(user),
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
if (!confirmed) {
return false;
}
this.actionPromise = this.deleteUser(user.id);
try {
await this.actionPromise;
this.platformUtilsService.showToast('success', null, this.i18nService.t('removedUserId',
this.userNamePipe.transform(user)));
this.removeUser(user);
} catch (e) {
this.validationService.showError(e);
}
this.actionPromise = null; this.actionPromise = null;
}
};
if (this.actionPromise != null) {
return;
} }
async reinvite(user: UserType) { try {
if (this.actionPromise != null) { const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
return; const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
}
this.actionPromise = this.reinviteUser(user.id); const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
try { if (autoConfirm == null || !autoConfirm) {
await this.actionPromise; const [modal] = await this.modalService.openViewRef(
this.platformUtilsService.showToast('success', null, this.i18nService.t('hasBeenReinvited', UserConfirmComponent,
this.userNamePipe.transform(user))); this.confirmModalRef,
} catch (e) { (comp) => {
this.validationService.showError(e); comp.name = this.userNamePipe.transform(user);
} comp.userId = user != null ? user.userId : null;
this.actionPromise = null; comp.publicKey = publicKey;
} comp.onConfirmedUser.subscribe(async () => {
try {
async confirm(user: UserType) { comp.formPromise = confirmUser(publicKey);
function updateUser(self: BasePeopleComponent<UserType>) { await comp.formPromise;
user.status = self.userStatusType.Confirmed; modal.close();
const mapIndex = self.statusMap.get(self.userStatusType.Accepted).indexOf(user); } catch (e) {
if (mapIndex > -1) {
self.statusMap.get(self.userStatusType.Accepted).splice(mapIndex, 1);
self.statusMap.get(self.userStatusType.Confirmed).push(user);
}
}
const confirmUser = async (publicKey: Uint8Array) => {
try {
this.actionPromise = this.confirmUser(user, publicKey);
await this.actionPromise;
updateUser(this);
this.platformUtilsService.showToast('success', null, this.i18nService.t('hasBeenConfirmed',
this.userNamePipe.transform(user)));
} catch (e) {
this.validationService.showError(e);
throw e;
} finally {
this.actionPromise = null;
}
};
if (this.actionPromise != null) {
return;
}
try {
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
const autoConfirm = await this.stateService.getAutoConfirmFingerPrints();
if (autoConfirm == null || !autoConfirm) {
const [modal] = await this.modalService.openViewRef(UserConfirmComponent, this.confirmModalRef, comp => {
comp.name = this.userNamePipe.transform(user);
comp.userId = user != null ? user.userId : null;
comp.publicKey = publicKey;
comp.onConfirmedUser.subscribe(async () => {
try {
comp.formPromise = confirmUser(publicKey);
await comp.formPromise;
modal.close();
} catch (e) {
this.logService.error(e);
}
});
});
return;
}
try {
const fingerprint = await this.cryptoService.getFingerprint(user.userId, publicKey.buffer);
this.logService.info(`User's fingerprint: ${fingerprint.join('-')}`);
} catch (e) {
this.logService.error(e); this.logService.error(e);
} }
await confirmUser(publicKey); });
} catch (e) { }
this.logService.error(`Handled exception: ${e}`); );
} return;
} }
isSearching() { try {
return this.searchService.isSearchable(this.searchText); const fingerprint = await this.cryptoService.getFingerprint(user.userId, publicKey.buffer);
this.logService.info(`User's fingerprint: ${fingerprint.join("-")}`);
} catch (e) {
this.logService.error(e);
}
await confirmUser(publicKey);
} catch (e) {
this.logService.error(`Handled exception: ${e}`);
} }
}
isPaging() { isSearching() {
const searching = this.isSearching(); return this.searchService.isSearchable(this.searchText);
if (searching && this.didScroll) { }
this.resetPaging();
} isPaging() {
return !searching && this.users && this.users.length > this.pageSize; const searching = this.isSearching();
if (searching && this.didScroll) {
this.resetPaging();
} }
return !searching && this.users && this.users.length > this.pageSize;
}
protected deleteWarningMessage(user: UserType): string { protected deleteWarningMessage(user: UserType): string {
return this.i18nService.t('removeUserConfirmation'); return this.i18nService.t("removeUserConfirmation");
}
protected getCheckedUsers() {
return this.users.filter((u) => (u as any).checked);
}
protected removeUser(user: UserType) {
let index = this.users.indexOf(user);
if (index > -1) {
this.users.splice(index, 1);
this.resetPaging();
} }
if (this.statusMap.has(user.status)) {
protected getCheckedUsers() { index = this.statusMap.get(user.status).indexOf(user);
return this.users.filter(u => (u as any).checked); if (index > -1) {
this.statusMap.get(user.status).splice(index, 1);
}
} }
}
protected removeUser(user: UserType) {
let index = this.users.indexOf(user);
if (index > -1) {
this.users.splice(index, 1);
this.resetPaging();
}
if (this.statusMap.has(user.status)) {
index = this.statusMap.get(user.status).indexOf(user);
if (index > -1) {
this.statusMap.get(user.status).splice(index, 1);
}
}
}
} }

View File

@@ -1,18 +1,30 @@
<div class="form-group mb-0"> <div class="form-group mb-0">
<div class="form-check mt-1 form-check-block"> <div class="form-check mt-1 form-check-block">
<input class="form-check-input" type="checkbox" [name]="pascalize(parentId)" [id]="parentId" <input
[(ngModel)]="parentChecked" [indeterminate]="parentIndeterminate"> class="form-check-input"
<label class="form-check-label font-weight-normal" [for]="parentId"> type="checkbox"
{{parentId | i18n}} [name]="pascalize(parentId)"
</label> [id]="parentId"
</div> [(ngModel)]="parentChecked"
<div class="form-group form-group-child-check mb-0"> [indeterminate]="parentIndeterminate"
<div class="form-check mt-1" *ngFor="let c of checkboxes"> />
<input class="form-check-input" type="checkbox" [name]="pascalize(c.id)" [id]="c.id" [ngModel]="c.get()" <label class="form-check-label font-weight-normal" [for]="parentId">
(ngModelChange)="c.set($event)"> {{ parentId | i18n }}
<label class="form-check-label font-weight-normal" [for]="c.id"> </label>
{{c.id | i18n}} </div>
</label> <div class="form-group form-group-child-check mb-0">
</div> <div class="form-check mt-1" *ngFor="let c of checkboxes">
<input
class="form-check-input"
type="checkbox"
[name]="pascalize(c.id)"
[id]="c.id"
[ngModel]="c.get()"
(ngModelChange)="c.set($event)"
/>
<label class="form-check-label font-weight-normal" [for]="c.id">
{{ c.id | i18n }}
</label>
</div> </div>
</div>
</div> </div>

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