mirror of
https://github.com/bitwarden/web
synced 2025-12-06 00:03:28 +00:00
Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef61652fba | ||
|
|
933a66b24c | ||
|
|
e2c6a5f8cd | ||
|
|
a818e7dd40 | ||
|
|
759dc647e5 | ||
|
|
37cf46d581 | ||
|
|
407032114e | ||
|
|
94aece134c | ||
|
|
7532bf9825 | ||
|
|
0f4f541b11 | ||
|
|
07a3d38bef | ||
|
|
e9273ff79a | ||
|
|
1aa708aed4 | ||
|
|
ebe5a6030e | ||
|
|
f6946085d8 | ||
|
|
beebe7c98b | ||
|
|
a51331d6b2 | ||
|
|
b7b970e654 | ||
|
|
d823e8522c | ||
|
|
6bc5ac46b7 | ||
|
|
1193a93f86 | ||
|
|
4cd052e009 | ||
|
|
949f61f1a4 | ||
|
|
2145c3f88c | ||
|
|
bb71d5dc0a | ||
|
|
41856ff6af | ||
|
|
a1388ddab7 | ||
|
|
b2d13f586d | ||
|
|
9f0cd586ee | ||
|
|
ce67497d3a | ||
|
|
0dc26e589a | ||
|
|
e14a676eea | ||
|
|
11cf89493d | ||
|
|
5be121ec71 | ||
|
|
95e58b5e69 | ||
|
|
506fd22280 | ||
|
|
d79b12dedc | ||
|
|
599cd7299c | ||
|
|
18d26b79af | ||
|
|
1f81b81a58 | ||
|
|
cc5e420484 | ||
|
|
b4eaa48765 | ||
|
|
76354741be | ||
|
|
1b466609f0 | ||
|
|
7e11b8bb5a | ||
|
|
b251e1f73c | ||
|
|
fa11382c08 | ||
|
|
e17a49acd5 | ||
|
|
bc71ffa6f2 | ||
|
|
95dc3c92c5 | ||
|
|
2135accaf4 | ||
|
|
429c38fc66 | ||
|
|
56e92b1695 | ||
|
|
b2685d455b | ||
|
|
abfd1fa254 | ||
|
|
24a5717e27 | ||
|
|
9d9503b00e | ||
|
|
7b0579ccf3 | ||
|
|
df84dff54f | ||
|
|
367c09f7e6 | ||
|
|
46967dc126 | ||
|
|
e0ede7ba74 | ||
|
|
1fe7554818 | ||
|
|
eff3332fef | ||
|
|
caea4775b3 | ||
|
|
5f04950358 | ||
|
|
c46af91240 | ||
|
|
f5034effd2 | ||
|
|
20408347fb | ||
|
|
49d5bfd3e7 | ||
|
|
e99d1a74fd | ||
|
|
43d1cede98 | ||
|
|
091fc93645 | ||
|
|
dfe2771ba7 | ||
|
|
d3664321fd | ||
|
|
2e01ff7826 | ||
|
|
59d5a7439d | ||
|
|
6e3edd75eb | ||
|
|
78992444bf | ||
|
|
f1dea8fb1a | ||
|
|
04e5ab0d01 | ||
|
|
22a1cef498 | ||
|
|
98eaeddbfd | ||
|
|
00e4df2dd3 | ||
|
|
cfb4133152 | ||
|
|
42361d17b5 | ||
|
|
02ee95506c | ||
|
|
a749946457 | ||
|
|
18fb86c243 | ||
|
|
7597e4006c | ||
|
|
50be5f4895 | ||
|
|
326fb47593 | ||
|
|
240c576bad | ||
|
|
88c8c8ae55 | ||
|
|
394a7e42fb | ||
|
|
869ee217eb | ||
|
|
03dbe272fc | ||
|
|
87973e9775 | ||
|
|
4450b1aa81 | ||
|
|
57575ea322 | ||
|
|
68d3d7abfd | ||
|
|
4502a966a1 | ||
|
|
e523733b2c | ||
|
|
3864f1d950 | ||
|
|
4bdb9c8632 | ||
|
|
b1c098614c | ||
|
|
4309064804 | ||
|
|
f91e67ad6b | ||
|
|
d63ec210c7 | ||
|
|
3d160ee1df | ||
|
|
51b482f57d | ||
|
|
b367c4b4ce | ||
|
|
7432ad310c | ||
|
|
5b02202efb | ||
|
|
23056bcd63 | ||
|
|
2b0c92a4ea | ||
|
|
d669d43fe4 | ||
|
|
426e0edfb5 | ||
|
|
2cc0aa6f3d | ||
|
|
f895916fbb | ||
|
|
fea3bba0df | ||
|
|
7ed7321219 | ||
|
|
b2bf192677 | ||
|
|
d323e775ca | ||
|
|
22a00b2341 | ||
|
|
f36bba6406 | ||
|
|
674c583881 | ||
|
|
eb5ad7c6dc | ||
|
|
ca771eb04c | ||
|
|
d705b8ab33 | ||
|
|
9454eda082 | ||
|
|
7d5329e186 | ||
|
|
18979a7f1a | ||
|
|
7301158e54 | ||
|
|
5b9c41f29a | ||
|
|
179884cf93 | ||
|
|
5bc01ea13e | ||
|
|
ca43db8d93 | ||
|
|
f4cb5e6632 | ||
|
|
da2e740e65 | ||
|
|
2f0d2bdf32 | ||
|
|
97eedb2034 | ||
|
|
3ac46e62cb | ||
|
|
97db3635af | ||
|
|
e3464da19a | ||
|
|
ec3ee8fbb3 | ||
|
|
96208d3760 | ||
|
|
5bb61c0730 | ||
|
|
858f86d9df | ||
|
|
aa1e5a11ad | ||
|
|
ded8865914 | ||
|
|
da1437a268 | ||
|
|
599f831a09 | ||
|
|
23b532e2bf | ||
|
|
9f1b8ae58f | ||
|
|
d62850f82d | ||
|
|
41a0cfd0a2 | ||
|
|
fb6e85c56b | ||
|
|
d58550c2b8 | ||
|
|
5bf3ca2708 | ||
|
|
3e4a7e7a56 | ||
|
|
5d17de227b | ||
|
|
0d985c0221 | ||
|
|
fba2102518 |
@@ -1,4 +1,4 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
@@ -1,4 +1,32 @@
|
||||
Code contributions are welcome! Please commit any pull requests against the `master` branch.
|
||||
# How to Contribute
|
||||
|
||||
Contributions of all kinds are welcome!
|
||||
|
||||
Please visit our [Community Forums](https://community.bitwarden.com/) for general community discussion and the development roadmap.
|
||||
|
||||
Here is how you can get involved:
|
||||
|
||||
* **Request a new feature:** Go to the [Feature Requests category](https://community.bitwarden.com/c/feature-requests/) of the Community Forums. Please search existing feature requests before making a new one
|
||||
|
||||
* **Write code for a new feature:** Make a new post in the [Github Contributions category](https://community.bitwarden.com/c/github-contributions/) of the Community Forums. Include a description of your proposed contribution, screeshots, and links to any relevant feature requests. This helps get feedback from the community and Bitwarden team members before you start writing code
|
||||
|
||||
* **Report a bug or submit a bugfix:** Use Github issues and pull requests
|
||||
|
||||
* **Write documentation:** Submit a pull request to the [Bitwarden help repository](https://github.com/bitwarden/help)
|
||||
|
||||
* **Help other users:** Go to the [User-to-User Support category](https://community.bitwarden.com/c/support/) on the Community Forums
|
||||
|
||||
* **Translate:** See the localization (l10n) section below
|
||||
|
||||
## Contributor Agreement
|
||||
|
||||
Please sign the [Contributor Agreement](https://cla-assistant.io/bitwarden/web) if you intend on contributing to any Github repository. Pull requests cannot be accepted and merged unless the author has signed the Contributor Agreement.
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
* use `npm run lint` and fix any linting suggestions before submitting a pull request
|
||||
* commit any pull requests against the `master` branch
|
||||
* include a link to your Community Forums post
|
||||
|
||||
# Localization (l10n)
|
||||
|
||||
|
||||
@@ -3,3 +3,50 @@ Please do not submit feature requests. The [Community Forums][1] has a
|
||||
section for submitting, voting for, and discussing product feature requests.
|
||||
[1]: https://community.bitwarden.com
|
||||
-->
|
||||
|
||||
## Describe the Bug
|
||||
|
||||
<!-- Comment:
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
## Steps To Reproduce
|
||||
|
||||
<!-- Comment:
|
||||
How can we reproduce the behavior:
|
||||
-->
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. Click on '...'
|
||||
|
||||
## Expected Result
|
||||
|
||||
<!-- Comment:
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
## Actual Result
|
||||
|
||||
<!-- Comment:
|
||||
A clear and concise description of what is happening.
|
||||
-->
|
||||
|
||||
## Screenshots or Videos
|
||||
|
||||
<!-- Comment:
|
||||
If applicable, add screenshots and/or a short video to help explain your problem.
|
||||
-->
|
||||
|
||||
## Environment
|
||||
|
||||
- Operating system: [e.g. Windows 10, Mac OS Catalina]
|
||||
- Browser: [e.g. Firefox 73.0.1]
|
||||
- Build Version (Bottom of the page): [2.13.0]
|
||||
|
||||
## Additional Context
|
||||
|
||||
<!-- Comment:
|
||||
Add any other context about the problem here.
|
||||
-->
|
||||
|
||||
@@ -9,3 +9,4 @@ files:
|
||||
zh-CN: zh_CN
|
||||
zh-TW: zh_TW
|
||||
en-GB: en_GB
|
||||
en-IN: en_IN
|
||||
|
||||
2
jslib
2
jslib
Submodule jslib updated: 0a30c7eb1e...79b856cb6e
2730
package-lock.json
generated
2730
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@@ -1,6 +1,8 @@
|
||||
{
|
||||
"name": "bitwarden-web",
|
||||
"version": "2.13.1",
|
||||
"version": "2.17.0",
|
||||
"license": "GPL-3.0",
|
||||
"repository": "https://github.com/bitwarden/web",
|
||||
"scripts": {
|
||||
"sub:init": "git submodule update --init --recursive",
|
||||
"sub:update": "git submodule update --remote",
|
||||
@@ -26,10 +28,11 @@
|
||||
"lint:fix": "tslint src/**/*.ts --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^7.2.11",
|
||||
"@ngtools/webpack": "^7.2.2",
|
||||
"@types/jquery": "^3.3.6",
|
||||
"@types/lunr": "^2.1.6",
|
||||
"@angular/compiler-cli": "^9.1.12",
|
||||
"@ngtools/webpack": "^9.1.12",
|
||||
"@types/jquery": "^3.5.1",
|
||||
"@types/lunr": "^2.3.3",
|
||||
"@types/node": "^10.17.28",
|
||||
"@types/node-forge": "^0.7.5",
|
||||
"@types/papaparse": "^4.5.3",
|
||||
"@types/webcrypto": "^0.0.28",
|
||||
@@ -41,45 +44,44 @@
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"del": "^3.0.0",
|
||||
"extract-text-webpack-plugin": "next",
|
||||
"file-loader": "^2.0.0",
|
||||
"gh-pages": "^1.2.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-google-webfonts": "^2.0.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-sass": "^4.13.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.0",
|
||||
"terser-webpack-plugin": "^1.2.3",
|
||||
"ts-loader": "^5.3.3",
|
||||
"tslint": "^5.12.1",
|
||||
"ts-loader": "^7.0.5",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-loader": "^3.5.4",
|
||||
"typescript": "3.2.4",
|
||||
"typescript": "3.8.3",
|
||||
"webpack": "^4.29.0",
|
||||
"webpack-cli": "^3.2.1",
|
||||
"webpack-dev-server": "^3.1.14"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "7.2.1",
|
||||
"@angular/cdk": "7.2.1",
|
||||
"@angular/common": "7.2.1",
|
||||
"@angular/compiler": "7.2.1",
|
||||
"@angular/core": "7.2.1",
|
||||
"@angular/forms": "7.2.1",
|
||||
"@angular/platform-browser": "7.2.1",
|
||||
"@angular/platform-browser-dynamic": "7.2.1",
|
||||
"@angular/router": "7.2.1",
|
||||
"@angular/upgrade": "7.2.1",
|
||||
"@angular/animations": "9.1.12",
|
||||
"@angular/cdk": "9.2.4",
|
||||
"@angular/common": "9.1.12",
|
||||
"@angular/compiler": "9.1.12",
|
||||
"@angular/core": "9.1.12",
|
||||
"@angular/forms": "9.1.12",
|
||||
"@angular/platform-browser": "9.1.12",
|
||||
"@angular/platform-browser-dynamic": "9.1.12",
|
||||
"@angular/router": "9.1.12",
|
||||
"@microsoft/signalr": "3.1.0",
|
||||
"@microsoft/signalr-protocol-msgpack": "3.1.0",
|
||||
"angular2-toaster": "6.1.0",
|
||||
"angulartics2": "6.3.0",
|
||||
"angular2-toaster": "8.0.0",
|
||||
"angulartics2": "9.1.0",
|
||||
"big-integer": "1.6.36",
|
||||
"bootstrap": "4.3.1",
|
||||
"braintree-web-drop-in": "1.13.0",
|
||||
"core-js": "2.6.2",
|
||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git",
|
||||
"duo_web_sdk": "git+https://github.com/duosecurity/duo_web_sdk.git#410a9186cc34663c4913b17d6528067cd3331f1d",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "3.4.1",
|
||||
"lunr": "2.3.3",
|
||||
@@ -88,12 +90,13 @@
|
||||
"papaparse": "4.6.0",
|
||||
"popper.js": "1.14.4",
|
||||
"qrious": "4.0.2",
|
||||
"rxjs": "6.3.3",
|
||||
"rxjs": "6.6.2",
|
||||
"sweetalert2": "9.8.1",
|
||||
"tslib": "^2.0.1",
|
||||
"web-animations-js": "2.3.1",
|
||||
"webcrypto-shim": "0.1.4",
|
||||
"whatwg-fetch": "3.0.0",
|
||||
"zone.js": "0.8.28",
|
||||
"zone.js": "0.10.3",
|
||||
"zxcvbn": "4.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<form (ngSubmit)="submit()" class="container" ngNativeValidate>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="text-center mb-4">
|
||||
@@ -25,9 +25,11 @@
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
<i class="fa fa-unlock-alt" aria-hidden="true"></i>
|
||||
{{'unlock' | i18n}}
|
||||
<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}}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LockService } from 'jslib/abstractions/lock.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
|
||||
|
||||
import { RouterService } from '../services/router.service';
|
||||
|
||||
@@ -23,11 +24,11 @@ export class LockComponent extends BaseLockComponent {
|
||||
constructor(router: Router, i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService, messagingService: MessagingService,
|
||||
userService: UserService, cryptoService: CryptoService,
|
||||
storageService: StorageService, lockService: LockService,
|
||||
storageService: StorageService, vaultTimeoutService: VaultTimeoutService,
|
||||
environmentService: EnvironmentService, private routerService: RouterService,
|
||||
stateService: StateService) {
|
||||
stateService: StateService, apiService: ApiService) {
|
||||
super(router, i18nService, platformUtilsService, messagingService, userService, cryptoService,
|
||||
storageService, lockService, environmentService, stateService);
|
||||
storageService, vaultTimeoutService, environmentService, stateService, apiService);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -44,6 +44,11 @@
|
||||
<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>
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
} from '@angular/router';
|
||||
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
@@ -20,8 +23,13 @@ export class LoginComponent extends BaseLoginComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
i18nService: I18nService, private route: ActivatedRoute,
|
||||
storageService: StorageService, stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, platformUtilsService, i18nService, storageService, stateService);
|
||||
platformUtilsService: PlatformUtilsService, environmentService: EnvironmentService,
|
||||
passwordGenerationService: PasswordGenerationService, cryptoFunctionService: CryptoFunctionService) {
|
||||
super(authService, router,
|
||||
platformUtilsService, i18nService,
|
||||
stateService, environmentService,
|
||||
passwordGenerationService, cryptoFunctionService,
|
||||
storageService);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,103 +1,156 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{'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" *ngIf="enforcedPolicyOptions">
|
||||
{{'masterPasswordPolicyInEffect' | i18n}}
|
||||
<ul class="mb-0">
|
||||
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireUpper">
|
||||
{{'policyInEffectUppercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireLower">
|
||||
{{'policyInEffectLowercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
|
||||
{{'policyInEffectNumbers' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
|
||||
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
|
||||
</ul>
|
||||
</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>
|
||||
<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>
|
||||
<small class="text-muted" *ngIf="showTerms">
|
||||
{{'submitAgreePolicies' | i18n}}
|
||||
<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>
|
||||
</small>
|
||||
<div class="layout" [ngClass]="['layout', layout]">
|
||||
<header class="header" *ngIf="layout === 'enterprise2'">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-7">
|
||||
<img alt="Bitwarden" class="logo mb-2" src="../../images/register-layout/logo-horizontal-white.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</header>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row">
|
||||
<div class="col-7" *ngIf="layout">
|
||||
<div class="mt-5">
|
||||
<div *ngIf="layout === 'enterprise2'">
|
||||
<h2>Companies globally trust Bitwarden for password management.</h2>
|
||||
<p>Start your 7-day free trial!</p>
|
||||
<p class="highlight">Quickly deploy your <b>organization</b></p>
|
||||
<p>Use Bitwarden across all platforms</p>
|
||||
<p>Collaborate and share securely</p>
|
||||
<figure>
|
||||
<figcaption>
|
||||
<cite>
|
||||
<img src="../../images/register-layout/wired-logo.png" alt="Wired">
|
||||
</cite>
|
||||
</figcaption>
|
||||
<blockquote>
|
||||
"Bitwarden has become a popular choice among open-source software advocates. After using
|
||||
it for a few months, I can see why." - February 2020
|
||||
</blockquote>
|
||||
</figure>
|
||||
</div>
|
||||
<div *ngIf="layout === 'enterprise3'">
|
||||
<p>Enterprise 3 layout</p>
|
||||
</div>
|
||||
<div *ngIf="layout === 'enterprise4'">
|
||||
<p>Enterprise 4 layout</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [ngClass]="{'col-5': layout, 'col-12': !layout}">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div [ngClass]="{'col-5': !layout, 'col-12': layout}">
|
||||
<p class="lead text-center mb-4" *ngIf="!layout">{{'createAccount' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body">
|
||||
<app-callout title="{{'createOrganizationStep1' | i18n}}" type="info"
|
||||
icon="fa-thumb-tack" *ngIf="showCreateOrgMessage">
|
||||
{{'createOrganizationCreatePersonalAccount' | i18n}}
|
||||
</app-callout>
|
||||
<div class="form-group">
|
||||
<label for="email">{{'emailAddress' | i18n}}</label>
|
||||
<input id="email" class="form-control" type="text" name="Email" [(ngModel)]="email"
|
||||
required [appAutofocus]="email === ''" inputmode="email"
|
||||
appInputVerbatim="false">
|
||||
<small class="form-text text-muted">{{'emailAddressDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="name">{{'yourName' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name"
|
||||
[appAutofocus]="email !== ''">
|
||||
<small class="form-text text-muted">{{'yourNameDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<app-callout type="info" *ngIf="enforcedPolicyOptions">
|
||||
{{'masterPasswordPolicyInEffect' | i18n}}
|
||||
<ul class="mb-0">
|
||||
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireUpper">
|
||||
{{'policyInEffectUppercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireLower">
|
||||
{{'policyInEffectLowercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
|
||||
{{'policyInEffectNumbers' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
|
||||
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
|
||||
</ul>
|
||||
</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 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>
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordP
|
||||
import { Policy } from 'jslib/models/domain/policy';
|
||||
|
||||
import { PolicyData } from 'jslib/models/data/policyData';
|
||||
import { ReferenceEventRequest } from 'jslib/models/request/referenceEventRequest';
|
||||
|
||||
@Component({
|
||||
selector: 'app-register',
|
||||
@@ -26,7 +27,7 @@ import { PolicyData } from 'jslib/models/data/policyData';
|
||||
})
|
||||
export class RegisterComponent extends BaseRegisterComponent {
|
||||
showCreateOrgMessage = false;
|
||||
showTerms = true;
|
||||
layout = '';
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
|
||||
private policies: Policy[];
|
||||
@@ -38,7 +39,6 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
passwordGenerationService: PasswordGenerationService, private policyService: PolicyService) {
|
||||
super(authService, router, i18nService, cryptoService, apiService, stateService, platformUtilsService,
|
||||
passwordGenerationService);
|
||||
this.showTerms = !platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
getPasswordScoreAlertDisplay() {
|
||||
@@ -63,6 +63,7 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
|
||||
async ngOnInit() {
|
||||
const queryParamsSub = this.route.queryParams.subscribe((qParams) => {
|
||||
this.referenceData = new ReferenceEventRequest();
|
||||
if (qParams.email != null && qParams.email.indexOf('@') > -1) {
|
||||
this.email = qParams.email;
|
||||
}
|
||||
@@ -70,9 +71,21 @@ export class RegisterComponent extends BaseRegisterComponent {
|
||||
this.stateService.save('loginRedirect', { route: '/settings/premium' });
|
||||
} else if (qParams.org != null) {
|
||||
this.showCreateOrgMessage = true;
|
||||
this.referenceData.flow = qParams.org;
|
||||
this.stateService.save('loginRedirect',
|
||||
{ route: '/settings/create-organization', qParams: { plan: qParams.org } });
|
||||
}
|
||||
if (qParams.layout != null) {
|
||||
this.layout = this.referenceData.layout = qParams.layout;
|
||||
}
|
||||
if (qParams.reference != null) {
|
||||
this.referenceData.id = qParams.reference;
|
||||
} else {
|
||||
this.referenceData.id = ('; ' + document.cookie).split('; reference=').pop().split(';').shift();
|
||||
}
|
||||
if (this.referenceData.id === '') {
|
||||
this.referenceData.id = null;
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
|
||||
85
src/app/accounts/set-password.component.html
Normal file
85
src/app/accounts/set-password.component.html
Normal file
@@ -0,0 +1,85 @@
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate autocomplete="off">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{'setMasterPassword' | i18n}}</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body text-center" *ngIf="syncLoading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
{{'loading' | i18n}}
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!syncLoading">
|
||||
<app-callout type="info">{{'ssoCompleteRegistration' | i18n}}</app-callout>
|
||||
<div class="form-group">
|
||||
<app-callout type="info" *ngIf="enforcedPolicyOptions">
|
||||
{{'masterPasswordPolicyInEffect' | i18n}}
|
||||
<ul class="mb-0">
|
||||
<li *ngIf="enforcedPolicyOptions?.minComplexity > 0">
|
||||
{{'policyInEffectMinComplexity' | i18n : getPasswordScoreAlertDisplay()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.minLength > 0">
|
||||
{{'policyInEffectMinLength' | i18n : enforcedPolicyOptions?.minLength.toString()}}
|
||||
</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireUpper">
|
||||
{{'policyInEffectUppercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireLower">
|
||||
{{'policyInEffectLowercase' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireNumbers">
|
||||
{{'policyInEffectNumbers' | i18n}}</li>
|
||||
<li *ngIf="enforcedPolicyOptions?.requireSpecial">
|
||||
{{'policyInEffectSpecial' | i18n : '!@#$%^&*'}}</li>
|
||||
</ul>
|
||||
</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>
|
||||
</form>
|
||||
34
src/app/accounts/set-password.component.ts
Normal file
34
src/app/accounts/set-password.component.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
SetPasswordComponent as BaseSetPasswordComponent,
|
||||
} from 'jslib/angular/components/set-password.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-set-password',
|
||||
templateUrl: 'set-password.component.html',
|
||||
})
|
||||
export class SetPasswordComponent extends BaseSetPasswordComponent {
|
||||
constructor(apiService: ApiService, i18nService: I18nService,
|
||||
cryptoService: CryptoService, messagingService: MessagingService,
|
||||
userService: UserService, passwordGenerationService: PasswordGenerationService,
|
||||
platformUtilsService: PlatformUtilsService, policyService: PolicyService, router: Router,
|
||||
syncService: SyncService, route: ActivatedRoute) {
|
||||
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
|
||||
platformUtilsService, policyService, router, apiService, syncService, route);
|
||||
}
|
||||
}
|
||||
33
src/app/accounts/sso.component.html
Normal file
33
src/app/accounts/sso.component.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<form #form (ngSubmit)="submit()" class="container" [appApiAction]="initiateSsoFormPromise" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<img src="../../images/logo-dark@2x.png" class="logo mb-2" alt="Bitwarden">
|
||||
<div class="card d-block mt-4">
|
||||
<div class="card-body" *ngIf="loggingIn">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
{{'loading' | i18n}}
|
||||
</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>
|
||||
</form>
|
||||
58
src/app/accounts/sso.component.ts
Normal file
58
src/app/accounts/sso.component.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
import { SsoComponent as BaseSsoComponent } from 'jslib/angular/components/sso.component';
|
||||
|
||||
const IdentifierStorageKey = 'ssoOrgIdentifier';
|
||||
|
||||
@Component({
|
||||
selector: 'app-sso',
|
||||
templateUrl: 'sso.component.html',
|
||||
})
|
||||
export class SsoComponent extends BaseSsoComponent {
|
||||
constructor(authService: AuthService, router: Router,
|
||||
i18nService: I18nService, route: ActivatedRoute,
|
||||
storageService: StorageService, stateService: StateService,
|
||||
platformUtilsService: PlatformUtilsService, apiService: ApiService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
passwordGenerationService: PasswordGenerationService) {
|
||||
super(authService, router, i18nService, route, storageService, stateService, platformUtilsService,
|
||||
apiService, cryptoFunctionService, passwordGenerationService);
|
||||
this.redirectUri = window.location.origin + '/sso-connector.html';
|
||||
this.clientId = 'web';
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
super.ngOnInit();
|
||||
const queryParamsSub = this.route.queryParams.subscribe(async (qParams) => {
|
||||
if (qParams.identifier != null) {
|
||||
this.identifier = qParams.identifier;
|
||||
} else {
|
||||
const storedIdentifier = await this.storageService.get<string>(IdentifierStorageKey);
|
||||
if (storedIdentifier != null) {
|
||||
this.identifier = storedIdentifier;
|
||||
}
|
||||
}
|
||||
if (queryParamsSub != null) {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.storageService.save(IdentifierStorageKey, this.identifier);
|
||||
super.submit();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { TwoFactorOptionsComponent } from './two-factor-options.component';
|
||||
|
||||
@@ -28,15 +31,15 @@ import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/comp
|
||||
templateUrl: 'two-factor.component.html',
|
||||
})
|
||||
export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef, static: true }) twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
i18nService: I18nService, apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService, stateService: StateService,
|
||||
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
storageService: StorageService) {
|
||||
storageService: StorageService, route: ActivatedRoute) {
|
||||
super(authService, router, i18nService, apiService, platformUtilsService, window, environmentService,
|
||||
stateService, storageService);
|
||||
stateService, storageService, route);
|
||||
this.onSuccessfulLoginNavigate = this.goAfterLogIn;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import { LoginComponent } from './accounts/login.component';
|
||||
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
|
||||
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
|
||||
import { RegisterComponent } from './accounts/register.component';
|
||||
import { SetPasswordComponent } from './accounts/set-password.component';
|
||||
import { SsoComponent } from './accounts/sso.component';
|
||||
import { TwoFactorComponent } from './accounts/two-factor.component';
|
||||
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
|
||||
import { VerifyRecoverDeleteComponent } from './accounts/verify-recover-delete.component';
|
||||
@@ -55,6 +57,9 @@ import {
|
||||
|
||||
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
|
||||
|
||||
import { AccessComponent } from './send/access.component';
|
||||
import { SendComponent } from './send/send.component';
|
||||
|
||||
import { AccountComponent } from './settings/account.component';
|
||||
import { CreateOrganizationComponent } from './settings/create-organization.component';
|
||||
import { DomainRulesComponent } from './settings/domain-rules.component';
|
||||
@@ -99,6 +104,15 @@ const routes: Routes = [
|
||||
canActivate: [UnauthGuardService],
|
||||
data: { titleId: 'createAccount' },
|
||||
},
|
||||
{
|
||||
path: 'sso', component: SsoComponent,
|
||||
canActivate: [UnauthGuardService],
|
||||
data: { titleId: 'enterpriseSingleSignOn' },
|
||||
},
|
||||
{
|
||||
path: 'set-password', component: SetPasswordComponent,
|
||||
data: { titleId: 'setMasterPassword' },
|
||||
},
|
||||
{
|
||||
path: 'hint', component: HintComponent,
|
||||
canActivate: [UnauthGuardService],
|
||||
@@ -130,6 +144,11 @@ const routes: Routes = [
|
||||
canActivate: [UnauthGuardService],
|
||||
data: { titleId: 'deleteAccount' },
|
||||
},
|
||||
{
|
||||
path: 'send/:sendId/:key',
|
||||
component: AccessComponent,
|
||||
data: { title: 'Bitwarden Send' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -138,6 +157,7 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuardService],
|
||||
children: [
|
||||
{ path: 'vault', component: VaultComponent, data: { titleId: 'myVault' } },
|
||||
// { path: 'sends', component: SendComponent, data: { title: 'Send' } },
|
||||
{
|
||||
path: 'settings',
|
||||
component: SettingsComponent,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as jq from 'jquery';
|
||||
import Swal from 'sweetalert2/src/sweetalert2.js';
|
||||
import Swal from 'sweetalert2/dist/sweetalert2.js';
|
||||
|
||||
import {
|
||||
BodyOutputType,
|
||||
@@ -35,7 +35,6 @@ import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { EventService } from 'jslib/abstractions/event.service';
|
||||
import { FolderService } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LockService } from 'jslib/abstractions/lock.service';
|
||||
import { NotificationsService } from 'jslib/abstractions/notifications.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
@@ -46,6 +45,7 @@ import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
|
||||
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
@@ -78,7 +78,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private authService: AuthService, private router: Router, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService, private ngZone: NgZone,
|
||||
private lockService: LockService, private storageService: StorageService,
|
||||
private vaultTimeoutService: VaultTimeoutService, private storageService: StorageService,
|
||||
private cryptoService: CryptoService, private collectionService: CollectionService,
|
||||
private sanitizer: DomSanitizer, private searchService: SearchService,
|
||||
private notificationsService: NotificationsService, private routerService: RouterService,
|
||||
@@ -110,7 +110,7 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.logOut(!!message.expired);
|
||||
break;
|
||||
case 'lockVault':
|
||||
await this.lockService.lock();
|
||||
await this.vaultTimeoutService.lock();
|
||||
break;
|
||||
case 'locked':
|
||||
this.notificationsService.updateConnection(false);
|
||||
@@ -148,6 +148,9 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
properties: { label: message.label },
|
||||
});
|
||||
break;
|
||||
case 'setFullWidth':
|
||||
this.setFullWidth();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -166,6 +169,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.setFullWidth();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
@@ -198,6 +203,8 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.toasterService.popAsync('warning', this.i18nService.t('loggedOut'),
|
||||
this.i18nService.t('loginExpired'));
|
||||
}
|
||||
|
||||
Swal.close();
|
||||
this.router.navigate(['/']);
|
||||
});
|
||||
}
|
||||
@@ -262,4 +269,13 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.notificationsService.reconnectFromActivity();
|
||||
}
|
||||
}
|
||||
|
||||
private async setFullWidth() {
|
||||
const enableFullWidth = await this.storageService.get<boolean>('enableFullWidth');
|
||||
if (enableFullWidth) {
|
||||
document.body.classList.add('full-width');
|
||||
} else {
|
||||
document.body.classList.remove('full-width');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ import { LoginComponent } from './accounts/login.component';
|
||||
import { RecoverDeleteComponent } from './accounts/recover-delete.component';
|
||||
import { RecoverTwoFactorComponent } from './accounts/recover-two-factor.component';
|
||||
import { RegisterComponent } from './accounts/register.component';
|
||||
import { SetPasswordComponent } from './accounts/set-password.component';
|
||||
import { SsoComponent } from './accounts/sso.component';
|
||||
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
|
||||
import { TwoFactorComponent } from './accounts/two-factor.component';
|
||||
import { VerifyEmailTokenComponent } from './accounts/verify-email-token.component';
|
||||
@@ -58,13 +60,11 @@ import { UserGroupsComponent as OrgUserGroupsComponent } from './organizations/m
|
||||
|
||||
import { AccountComponent as OrgAccountComponent } from './organizations/settings/account.component';
|
||||
import { AdjustSeatsComponent } from './organizations/settings/adjust-seats.component';
|
||||
import { ApiKeyComponent as OrgApiKeyComponent } from './organizations/settings/api-key.component';
|
||||
import { ChangePlanComponent } from './organizations/settings/change-plan.component';
|
||||
import { DeleteOrganizationComponent } from './organizations/settings/delete-organization.component';
|
||||
import { DownloadLicenseComponent } from './organizations/settings/download-license.component';
|
||||
import { OrganizationBillingComponent } from './organizations/settings/organization-billing.component';
|
||||
import { OrganizationSubscriptionComponent } from './organizations/settings/organization-subscription.component';
|
||||
import { RotateApiKeyComponent as OrgRotateApiKeyComponent } from './organizations/settings/rotate-api-key.component';
|
||||
import { SettingsComponent as OrgSettingComponent } from './organizations/settings/settings.component';
|
||||
import {
|
||||
TwoFactorSetupComponent as OrgTwoFactorSetupComponent,
|
||||
@@ -96,10 +96,15 @@ import { CollectionsComponent as OrgCollectionsComponent } from './organizations
|
||||
import { GroupingsComponent as OrgGroupingsComponent } from './organizations/vault/groupings.component';
|
||||
import { VaultComponent as OrgVaultComponent } from './organizations/vault/vault.component';
|
||||
|
||||
import { AccessComponent } from './send/access.component';
|
||||
import { AddEditComponent as SendAddEditComponent } from './send/add-edit.component';
|
||||
import { SendComponent } from './send/send.component';
|
||||
|
||||
import { AccountComponent } from './settings/account.component';
|
||||
import { AddCreditComponent } from './settings/add-credit.component';
|
||||
import { AdjustPaymentComponent } from './settings/adjust-payment.component';
|
||||
import { AdjustStorageComponent } from './settings/adjust-storage.component';
|
||||
import { ApiKeyComponent } from './settings/api-key.component';
|
||||
import { ChangeEmailComponent } from './settings/change-email.component';
|
||||
import { ChangeKdfComponent } from './settings/change-kdf.component';
|
||||
import { ChangePasswordComponent } from './settings/change-password.component';
|
||||
@@ -107,6 +112,7 @@ import { CreateOrganizationComponent } from './settings/create-organization.comp
|
||||
import { DeauthorizeSessionsComponent } from './settings/deauthorize-sessions.component';
|
||||
import { DeleteAccountComponent } from './settings/delete-account.component';
|
||||
import { DomainRulesComponent } from './settings/domain-rules.component';
|
||||
import { LinkSsoComponent } from './settings/link-sso.component';
|
||||
import { OptionsComponent } from './settings/options.component';
|
||||
import { OrganizationPlansComponent } from './settings/organization-plans.component';
|
||||
import { OrganizationsComponent } from './settings/organizations.component';
|
||||
@@ -115,6 +121,7 @@ import { PremiumComponent } from './settings/premium.component';
|
||||
import { ProfileComponent } from './settings/profile.component';
|
||||
import { PurgeVaultComponent } from './settings/purge-vault.component';
|
||||
import { SettingsComponent } from './settings/settings.component';
|
||||
import { TaxInfoComponent } from './settings/tax-info.component';
|
||||
import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component';
|
||||
import { TwoFactorDuoComponent } from './settings/two-factor-duo.component';
|
||||
import { TwoFactorEmailComponent } from './settings/two-factor-email.component';
|
||||
@@ -143,8 +150,10 @@ import { WeakPasswordsReportComponent } from './tools/weak-passwords-report.comp
|
||||
|
||||
import { AddEditComponent } from './vault/add-edit.component';
|
||||
import { AttachmentsComponent } from './vault/attachments.component';
|
||||
import { BulkActionsComponent } from './vault/bulk-actions.component';
|
||||
import { BulkDeleteComponent } from './vault/bulk-delete.component';
|
||||
import { BulkMoveComponent } from './vault/bulk-move.component';
|
||||
import { BulkRestoreComponent } from './vault/bulk-restore.component';
|
||||
import { BulkShareComponent } from './vault/bulk-share.component';
|
||||
import { CiphersComponent } from './vault/ciphers.component';
|
||||
import { CollectionsComponent } from './vault/collections.component';
|
||||
@@ -173,11 +182,15 @@ import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
|
||||
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
|
||||
import { SearchPipe } from 'jslib/angular/pipes/search.pipe';
|
||||
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import {
|
||||
registerLocaleData,
|
||||
DatePipe,
|
||||
} from '@angular/common';
|
||||
import localeCa from '@angular/common/locales/ca';
|
||||
import localeCs from '@angular/common/locales/cs';
|
||||
import localeDa from '@angular/common/locales/da';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeEl from '@angular/common/locales/el';
|
||||
import localeEnGb from '@angular/common/locales/en-GB';
|
||||
import localeEs from '@angular/common/locales/es';
|
||||
import localeEt from '@angular/common/locales/et';
|
||||
@@ -186,6 +199,8 @@ import localeHe from '@angular/common/locales/he';
|
||||
import localeIt from '@angular/common/locales/it';
|
||||
import localeJa from '@angular/common/locales/ja';
|
||||
import localeKo from '@angular/common/locales/ko';
|
||||
import localeLv from '@angular/common/locales/lv';
|
||||
import localeMl from '@angular/common/locales/ml';
|
||||
import localeNb from '@angular/common/locales/nb';
|
||||
import localeNl from '@angular/common/locales/nl';
|
||||
import localePl from '@angular/common/locales/pl';
|
||||
@@ -202,6 +217,7 @@ registerLocaleData(localeCa, 'ca');
|
||||
registerLocaleData(localeCs, 'cs');
|
||||
registerLocaleData(localeDa, 'da');
|
||||
registerLocaleData(localeDe, 'de');
|
||||
registerLocaleData(localeEl, 'el');
|
||||
registerLocaleData(localeEnGb, 'en-GB');
|
||||
registerLocaleData(localeEs, 'es');
|
||||
registerLocaleData(localeEt, 'et');
|
||||
@@ -210,6 +226,8 @@ registerLocaleData(localeHe, 'he');
|
||||
registerLocaleData(localeIt, 'it');
|
||||
registerLocaleData(localeJa, 'ja');
|
||||
registerLocaleData(localeKo, 'ko');
|
||||
registerLocaleData(localeLv, 'lv');
|
||||
registerLocaleData(localeMl, 'ml');
|
||||
registerLocaleData(localeNb, 'nb');
|
||||
registerLocaleData(localeNl, 'nl');
|
||||
registerLocaleData(localePl, 'pl');
|
||||
@@ -229,7 +247,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
FormsModule,
|
||||
AppRoutingModule,
|
||||
ServicesModule,
|
||||
Angulartics2Module.forRoot([Angulartics2GoogleAnalytics], {
|
||||
Angulartics2Module.forRoot({
|
||||
pageTracking: {
|
||||
clearQueryParams: true,
|
||||
},
|
||||
@@ -240,14 +258,17 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
],
|
||||
declarations: [
|
||||
A11yTitleDirective,
|
||||
AccessComponent,
|
||||
AcceptOrganizationComponent,
|
||||
AccountComponent,
|
||||
SetPasswordComponent,
|
||||
AddCreditComponent,
|
||||
AddEditComponent,
|
||||
AdjustPaymentComponent,
|
||||
AdjustSeatsComponent,
|
||||
AdjustStorageComponent,
|
||||
ApiActionDirective,
|
||||
ApiKeyComponent,
|
||||
AppComponent,
|
||||
AttachmentsComponent,
|
||||
AutofocusDirective,
|
||||
@@ -255,8 +276,10 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
BlurClickDirective,
|
||||
BoxRowDirective,
|
||||
BreachReportComponent,
|
||||
BulkActionsComponent,
|
||||
BulkDeleteComponent,
|
||||
BulkMoveComponent,
|
||||
BulkRestoreComponent,
|
||||
BulkShareComponent,
|
||||
CalloutComponent,
|
||||
ChangeEmailComponent,
|
||||
@@ -285,6 +308,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
ImportComponent,
|
||||
InactiveTwoFactorReportComponent,
|
||||
InputVerbatimDirective,
|
||||
LinkSsoComponent,
|
||||
LockComponent,
|
||||
LoginComponent,
|
||||
ModalComponent,
|
||||
@@ -292,7 +316,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
OptionsComponent,
|
||||
OrgAccountComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgApiKeyComponent,
|
||||
OrganizationBillingComponent,
|
||||
OrganizationPlansComponent,
|
||||
OrganizationSubscriptionComponent,
|
||||
@@ -316,7 +339,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
OrgPolicyEditComponent,
|
||||
OrgPoliciesComponent,
|
||||
OrgReusedPasswordsReportComponent,
|
||||
OrgRotateApiKeyComponent,
|
||||
OrgSettingComponent,
|
||||
OrgToolsComponent,
|
||||
OrgTwoFactorSetupComponent,
|
||||
@@ -342,10 +364,14 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
SearchCiphersPipe,
|
||||
SearchPipe,
|
||||
SelectCopyDirective,
|
||||
SendAddEditComponent,
|
||||
SendComponent,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
SsoComponent,
|
||||
StopClickDirective,
|
||||
StopPropDirective,
|
||||
TaxInfoComponent,
|
||||
ToolsComponent,
|
||||
TrueFalseValueDirective,
|
||||
TwoFactorAuthenticatorComponent,
|
||||
@@ -372,9 +398,12 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
],
|
||||
entryComponents: [
|
||||
AddEditComponent,
|
||||
ApiKeyComponent,
|
||||
AttachmentsComponent,
|
||||
BulkActionsComponent,
|
||||
BulkDeleteComponent,
|
||||
BulkMoveComponent,
|
||||
BulkRestoreComponent,
|
||||
BulkShareComponent,
|
||||
CollectionsComponent,
|
||||
DeauthorizeSessionsComponent,
|
||||
@@ -383,7 +412,6 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
FolderAddEditComponent,
|
||||
ModalComponent,
|
||||
OrgAddEditComponent,
|
||||
OrgApiKeyComponent,
|
||||
OrgAttachmentsComponent,
|
||||
OrgCollectionAddEditComponent,
|
||||
OrgCollectionsComponent,
|
||||
@@ -391,12 +419,12 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
OrgEntityUsersComponent,
|
||||
OrgGroupAddEditComponent,
|
||||
OrgPolicyEditComponent,
|
||||
OrgRotateApiKeyComponent,
|
||||
OrgUserAddEditComponent,
|
||||
OrgUserConfirmComponent,
|
||||
OrgUserGroupsComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PurgeVaultComponent,
|
||||
SendAddEditComponent,
|
||||
ShareComponent,
|
||||
TwoFactorAuthenticatorComponent,
|
||||
TwoFactorDuoComponent,
|
||||
@@ -407,7 +435,7 @@ registerLocaleData(localeZhTw, 'zh-TW');
|
||||
TwoFactorYubiKeyComponent,
|
||||
UpdateKeyComponent,
|
||||
],
|
||||
providers: [],
|
||||
providers: [DatePipe],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule { }
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/vault">{{'myVault' | i18n}}</a>
|
||||
</li>
|
||||
<!--<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/sends">Send</a>
|
||||
</li>-->
|
||||
<li class="nav-item" routerLinkActive="active">
|
||||
<a class="nav-link" routerLink="/tools">{{'tools' | i18n}}</a>
|
||||
</li>
|
||||
@@ -39,7 +42,7 @@
|
||||
<i class="fa fa-fw fa-question-circle" aria-hidden="true"></i>
|
||||
{{'getHelp' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="https://bitwarden.com#download" target="_blank" rel="noopener">
|
||||
<a class="dropdown-item" href="https://bitwarden.com/download/" target="_blank" rel="noopener">
|
||||
<i class="fa fa-fw fa-download" aria-hidden="true"></i>
|
||||
{{'getApps' | i18n}}
|
||||
</a>
|
||||
|
||||
@@ -1,45 +1,56 @@
|
||||
<app-navbar></app-navbar>
|
||||
<div class="org-nav" *ngIf="organization">
|
||||
<div class="container d-flex flex-column">
|
||||
<div class="my-auto d-flex align-items-center pl-1">
|
||||
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
|
||||
<div class="org-name ml-3">
|
||||
<span>{{organization.name}}</span>
|
||||
<small class="text-muted">{{'organization' | i18n}}</small>
|
||||
</div>
|
||||
<div class="ml-auto card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
|
||||
<div class="card-body py-2">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{'organizationIsDisabled' | i18n}}
|
||||
<div class="container d-flex">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="my-auto d-flex align-items-center pl-1">
|
||||
<app-avatar [data]="organization.name" size="45" [circle]="true"></app-avatar>
|
||||
<div class="org-name ml-3">
|
||||
<span>{{organization.name}}</span>
|
||||
<small class="text-muted">{{'organization' | i18n}}</small>
|
||||
</div>
|
||||
<div class="ml-3 card border-danger text-danger bg-transparent" *ngIf="!organization.enabled">
|
||||
<div class="card-body py-2">
|
||||
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i>
|
||||
{{'organizationIsDisabled' | i18n}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" *ngIf="organization.isManager">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
||||
<i class="fa fa-lock" aria-hidden="true"></i>
|
||||
{{'vault' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="manage" routerLinkActive="active">
|
||||
<i class="fa fa-sliders" aria-hidden="true"></i>
|
||||
{{'manage' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isAdmin">
|
||||
<a class="nav-link" routerLink="tools" routerLinkActive="active">
|
||||
<i class="fa fa-wrench" aria-hidden="true"></i>
|
||||
{{'tools' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isOwner">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
{{'settings' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="ml-auto d-flex align-items-center">
|
||||
<button class="btn btn-primary" (click)="goToEnterprisePortal()" #enterpriseBtn
|
||||
[appApiAction]="enterpriseTokenPromise" *ngIf="organization.useBusinessPortal">
|
||||
<i class="fa fa-bank fa-fw" [hidden]="enterpriseBtn.loading" aria-hidden="true"></i>
|
||||
<i class="fa fa-spinner fa-spin fa-fw" [hidden]="!enterpriseBtn.loading" title="{{'loading' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
{{'businessPortal' | i18n}} →
|
||||
</button>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" *ngIf="organization.isManager">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="vault" routerLinkActive="active">
|
||||
<i class="fa fa-lock" aria-hidden="true"></i>
|
||||
{{'vault' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" routerLink="manage" routerLinkActive="active">
|
||||
<i class="fa fa-sliders" aria-hidden="true"></i>
|
||||
{{'manage' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isAdmin">
|
||||
<a class="nav-link" routerLink="tools" routerLinkActive="active">
|
||||
<i class="fa fa-wrench" aria-hidden="true"></i>
|
||||
{{'tools' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item" *ngIf="organization.isOwner">
|
||||
<a class="nav-link" routerLink="settings" routerLinkActive="active">
|
||||
<i class="fa fa-cogs" aria-hidden="true"></i>
|
||||
{{'settings' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<router-outlet></router-outlet>
|
||||
|
||||
@@ -4,10 +4,14 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
@@ -20,19 +24,28 @@ const BroadcasterSubscriptionId = 'OrganizationLayoutComponent';
|
||||
})
|
||||
export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
organization: Organization;
|
||||
|
||||
enterpriseTokenPromise: Promise<any>;
|
||||
private organizationId: string;
|
||||
private enterpriseUrl: string;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone,
|
||||
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
|
||||
private environmentService: EnvironmentService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.enterpriseUrl = 'https://portal.bitwarden.com';
|
||||
if (this.environmentService.enterpriseUrl != null) {
|
||||
this.enterpriseUrl = this.environmentService.enterpriseUrl;
|
||||
} else if (this.environmentService.baseUrl != null) {
|
||||
this.enterpriseUrl = this.environmentService.baseUrl + '/portal';
|
||||
}
|
||||
|
||||
document.body.classList.remove('layout_frontend');
|
||||
this.route.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
|
||||
this.ngZone.run(async () => {
|
||||
switch (message.command) {
|
||||
@@ -51,4 +64,20 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy {
|
||||
async load() {
|
||||
this.organization = await this.userService.getOrganization(this.organizationId);
|
||||
}
|
||||
|
||||
async goToEnterprisePortal() {
|
||||
if (this.enterpriseTokenPromise != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.enterpriseTokenPromise = this.apiService.getEnterprisePortalSignInToken();
|
||||
const token = await this.enterpriseTokenPromise;
|
||||
if (token != null) {
|
||||
const userId = await this.userService.getUserId();
|
||||
this.platformUtilsService.launchUri(this.enterpriseUrl + '/login?userId=' + userId +
|
||||
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organization.id);
|
||||
}
|
||||
} catch { }
|
||||
this.enterpriseTokenPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
<div class="modal-body" *ngIf="!loading">
|
||||
<div class="form-group">
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="name" required
|
||||
appAutofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="externalId">{{'externalId' | i18n}}</label>
|
||||
@@ -41,6 +42,7 @@
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>{{'name' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -58,6 +60,10 @@
|
||||
<span class="sr-only">{{'groupAccessAllItems' | i18n}}</span>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="g.hidePasswords"
|
||||
name="Groups[{{i}}].HidePasswords" [disabled]="!g.checked || g.accessAll">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="g.readOnly" name="Groups[{{i}}].ReadOnly"
|
||||
[disabled]="!g.checked || g.accessAll">
|
||||
|
||||
@@ -73,6 +73,7 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
if (group != null && group.length > 0) {
|
||||
(group[0] as any).checked = true;
|
||||
(group[0] as any).readOnly = s.readOnly;
|
||||
(group[0] as any).hidePasswords = s.hidePasswords;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -97,6 +98,7 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
(g as any).checked = select == null ? !(g as any).checked : select;
|
||||
if (!(g as any).checked) {
|
||||
(g as any).readOnly = false;
|
||||
(g as any).hidePasswords = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +115,7 @@ export class CollectionAddEditComponent implements OnInit {
|
||||
request.name = (await this.cryptoService.encrypt(this.name, this.orgKey)).encryptedString;
|
||||
request.externalId = this.externalId;
|
||||
request.groups = this.groups.filter((g) => (g as any).checked && !g.accessAll)
|
||||
.map((g) => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly));
|
||||
.map((g) => new SelectionReadOnlyRequest(g.id, !!(g as any).readOnly, !!(g as any).hidePasswords));
|
||||
|
||||
try {
|
||||
if (this.editMode) {
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && (collections | search:searchText:'name':'id') as searchedCollections">
|
||||
<ng-container
|
||||
*ngIf="!loading && (isPaging() ? pagedCollections : collections | search:searchText:'name':'id') as searchedCollections">
|
||||
<p *ngIf="!searchedCollections.length">{{'noCollectionsInList' | i18n}}</p>
|
||||
<table class="table table-hover table-list" *ngIf="searchedCollections.length">
|
||||
<table class="table table-hover table-list" *ngIf="searchedCollections.length" infiniteScroll
|
||||
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||
<tbody>
|
||||
<tr *ngFor="let c of searchedCollections">
|
||||
<td>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CollectionService } from 'jslib/abstractions/collection.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { CollectionData } from 'jslib/models/data/collectionData';
|
||||
@@ -34,21 +35,26 @@ import { EntityUsersComponent } from './entity-users.component';
|
||||
templateUrl: 'collections.component.html',
|
||||
})
|
||||
export class CollectionsComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef }) usersModalRef: ViewContainerRef;
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
collections: CollectionView[];
|
||||
pagedCollections: CollectionView[];
|
||||
searchText: string;
|
||||
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
|
||||
private pagedCollectionsCount = 0;
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
||||
private collectionService: CollectionService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
|
||||
private userService: UserService) { }
|
||||
private userService: UserService, private searchService: SearchService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
@@ -74,9 +80,27 @@ export class CollectionsComponent implements OnInit {
|
||||
const collections = response.data.filter((c) => c.organizationId === this.organizationId).map((r) =>
|
||||
new Collection(new CollectionData(r as CollectionDetailsResponse)));
|
||||
this.collections = await this.collectionService.decryptMany(collections);
|
||||
this.resetPaging();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (!this.collections || this.collections.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedCollections.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedCollectionsCount > this.pageSize) {
|
||||
pagedSize = this.pagedCollectionsCount;
|
||||
}
|
||||
if (this.collections.length > pagedLength) {
|
||||
this.pagedCollections =
|
||||
this.pagedCollections.concat(this.collections.slice(pagedLength, pagedLength + pagedSize));
|
||||
}
|
||||
this.pagedCollectionsCount = this.pagedCollections.length;
|
||||
this.didScroll = this.pagedCollections.length > this.pageSize;
|
||||
}
|
||||
|
||||
edit(collection: CollectionView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
@@ -147,10 +171,28 @@ export class CollectionsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
async resetPaging() {
|
||||
this.pagedCollections = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return this.searchService.isSearchable(this.searchText);
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
const searching = this.isSearching();
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.collections && this.collections.length > this.pageSize;
|
||||
}
|
||||
|
||||
private removeCollection(collection: CollectionView) {
|
||||
const index = this.collections.indexOf(collection);
|
||||
if (index > -1) {
|
||||
this.collections.splice(index, 1);
|
||||
this.resetPaging();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
<th>{{'name' | i18n}}</th>
|
||||
<th *ngIf="entity === 'collection'"> </th>
|
||||
<th>{{'userType' | i18n}}</th>
|
||||
<th width="100" class="text-center" *ngIf="entity === 'collection'">{{'hidePasswords' |
|
||||
i18n}}</th>
|
||||
<th width="100" class="text-center" *ngIf="entity === 'collection'">{{'readOnly' |
|
||||
i18n}}</th>
|
||||
</tr>
|
||||
@@ -85,6 +87,11 @@
|
||||
<span *ngIf="u.type === organizationUserType.Manager">{{'manager' | i18n}}</span>
|
||||
<span *ngIf="u.type === organizationUserType.User">{{'user' | i18n}}</span>
|
||||
</td>
|
||||
<td class="text-center" *ngIf="entity === 'collection'">
|
||||
<input type="checkbox" [(ngModel)]="u.hidePasswords"
|
||||
name="{{u.id.substr(0,8)}}_HidePasswords"
|
||||
[disabled]="u.accessAll || !u.checked">
|
||||
</td>
|
||||
<td class="text-center" *ngIf="entity === 'collection'">
|
||||
<input type="checkbox" [(ngModel)]="u.readOnly" name="{{u.id.substr(0,8)}}_ReadOnly"
|
||||
[disabled]="u.accessAll || !u.checked">
|
||||
|
||||
@@ -78,6 +78,7 @@ export class EntityUsersComponent implements OnInit {
|
||||
if (user != null && user.length > 0) {
|
||||
(user[0] as any).checked = true;
|
||||
(user[0] as any).readOnly = s.readOnly;
|
||||
(user[0] as any).hidePasswords = s.hidePasswords;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -107,6 +108,7 @@ export class EntityUsersComponent implements OnInit {
|
||||
} else {
|
||||
if (this.entity === 'collection') {
|
||||
(u as any).readOnly = false;
|
||||
(u as any).hidePasswords = false;
|
||||
}
|
||||
this.selectedCount--;
|
||||
}
|
||||
@@ -123,7 +125,7 @@ export class EntityUsersComponent implements OnInit {
|
||||
this.formPromise = this.apiService.putGroupUsers(this.organizationId, this.entityId, selections);
|
||||
} else {
|
||||
const selections = this.users.filter((u) => (u as any).checked && !u.accessAll)
|
||||
.map((u) => new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly));
|
||||
.map((u) => new SelectionReadOnlyRequest(u.id, !!(u as any).readOnly, !!(u as any).hidePasswords));
|
||||
this.formPromise = this.apiService.putCollectionUsers(this.organizationId, this.entityId, selections);
|
||||
}
|
||||
await this.formPromise;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="groupAddEditTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="groupAddEditTitle">{{title}}</h2>
|
||||
@@ -24,6 +24,10 @@
|
||||
<h3 class="mt-4 d-flex">
|
||||
<div class="mb-2">
|
||||
{{'accessControl' | i18n}}
|
||||
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||
href="https://bitwarden.com/help/article/user-types-access-control/#access-control">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
|
||||
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
|
||||
@@ -59,6 +63,7 @@
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>{{'name' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -71,6 +76,10 @@
|
||||
<td (click)="check(c)">
|
||||
{{c.name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.hidePasswords"
|
||||
name="Collection[{{i}}].HidePasswords" [disabled]="!c.checked">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
|
||||
[disabled]="!c.checked">
|
||||
|
||||
@@ -63,6 +63,7 @@ export class GroupAddEditComponent implements OnInit {
|
||||
if (collection != null && collection.length > 0) {
|
||||
(collection[0] as any).checked = true;
|
||||
collection[0].readOnly = s.readOnly;
|
||||
collection[0].hidePasswords = s.hidePasswords;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -99,7 +100,7 @@ export class GroupAddEditComponent implements OnInit {
|
||||
request.accessAll = this.access === 'all';
|
||||
if (!request.accessAll) {
|
||||
request.collections = this.collections.filter((c) => (c as any).checked)
|
||||
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly));
|
||||
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && (groups | search:searchText:'name':'id') as searchedGroups">
|
||||
<ng-container *ngIf="!loading && (isPaging() ? pagedGroups : groups | search:searchText:'name':'id') as searchedGroups">
|
||||
<p *ngIf="!searchedGroups.length">{{'noGroupsInList' | i18n}}</p>
|
||||
<table class="table table-hover table-list" *ngIf="searchedGroups.length">
|
||||
<table class="table table-hover table-list" *ngIf="searchedGroups.length" infiniteScroll
|
||||
[infiniteScrollDistance]="1" [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||
<tbody>
|
||||
<tr *ngFor="let g of searchedGroups">
|
||||
<td>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Angulartics2 } from 'angulartics2';
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { GroupResponse } from 'jslib/models/response/groupResponse';
|
||||
@@ -31,21 +32,26 @@ import { GroupAddEditComponent } from './group-add-edit.component';
|
||||
templateUrl: 'groups.component.html',
|
||||
})
|
||||
export class GroupsComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef }) usersModalRef: ViewContainerRef;
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('usersTemplate', { read: ViewContainerRef, static: true }) usersModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
groups: GroupResponse[];
|
||||
pagedGroups: GroupResponse[];
|
||||
searchText: string;
|
||||
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
|
||||
private pagedGroupsCount = 0;
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private platformUtilsService: PlatformUtilsService, private userService: UserService,
|
||||
private router: Router) { }
|
||||
private router: Router, private searchService: SearchService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
@@ -70,9 +76,26 @@ export class GroupsComponent implements OnInit {
|
||||
const groups = response.data != null && response.data.length > 0 ? response.data : [];
|
||||
groups.sort(Utils.getSortFunction(this.i18nService, 'name'));
|
||||
this.groups = groups;
|
||||
this.resetPaging();
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
loadMore() {
|
||||
if (!this.groups || this.groups.length <= this.pageSize) {
|
||||
return;
|
||||
}
|
||||
const pagedLength = this.pagedGroups.length;
|
||||
let pagedSize = this.pageSize;
|
||||
if (pagedLength === 0 && this.pagedGroupsCount > this.pageSize) {
|
||||
pagedSize = this.pagedGroupsCount;
|
||||
}
|
||||
if (this.groups.length > pagedLength) {
|
||||
this.pagedGroups = this.pagedGroups.concat(this.groups.slice(pagedLength, pagedLength + pagedSize));
|
||||
}
|
||||
this.pagedGroupsCount = this.pagedGroups.length;
|
||||
this.didScroll = this.pagedGroups.length > this.pageSize;
|
||||
}
|
||||
|
||||
edit(group: GroupResponse) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
@@ -142,10 +165,28 @@ export class GroupsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
async resetPaging() {
|
||||
this.pagedGroups = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return this.searchService.isSearchable(this.searchText);
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
const searching = this.isSearching();
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.groups && this.groups.length > this.pageSize;
|
||||
}
|
||||
|
||||
private removeGroup(group: GroupResponse) {
|
||||
const index = this.groups.indexOf(group);
|
||||
if (index > -1) {
|
||||
this.groups.splice(index, 1);
|
||||
this.resetPaging();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,15 @@
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!loading && (users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||
<ng-container
|
||||
*ngIf="!loading && (isPaging() ? pagedUsers : users | search:searchText:'name':'email':'id') as searchedUsers">
|
||||
<p *ngIf="!searchedUsers.length">{{'noUsersInList' | i18n}}</p>
|
||||
<ng-container *ngIf="searchedUsers.length">
|
||||
<app-callout type="info" title="{{'confirmUsers' | i18n}}" icon="fa-check-circle" *ngIf="showConfirmUsers">
|
||||
{{'usersNeedConfirmed' | i18n}}
|
||||
</app-callout>
|
||||
<table class="table table-hover table-list">
|
||||
<table class="table table-hover table-list" infiniteScroll [infiniteScrollDistance]="1"
|
||||
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||
<tbody>
|
||||
<tr *ngFor="let u of searchedUsers">
|
||||
<td width="30">
|
||||
|
||||
@@ -19,6 +19,7 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SearchService } from 'jslib/abstractions/search.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@@ -42,14 +43,15 @@ import { UserGroupsComponent } from './user-groups.component';
|
||||
templateUrl: 'people.component.html',
|
||||
})
|
||||
export class PeopleComponent implements OnInit {
|
||||
@ViewChild('addEdit', { read: ViewContainerRef }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('groupsTemplate', { read: ViewContainerRef }) groupsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: ViewContainerRef;
|
||||
@ViewChild('confirmTemplate', { read: ViewContainerRef }) confirmModalRef: ViewContainerRef;
|
||||
@ViewChild('addEdit', { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||
@ViewChild('groupsTemplate', { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
||||
@ViewChild('confirmTemplate', { read: ViewContainerRef, static: true }) confirmModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
users: OrganizationUserUserDetailsResponse[];
|
||||
pagedUsers: OrganizationUserUserDetailsResponse[];
|
||||
searchText: string;
|
||||
status: OrganizationUserStatusType = null;
|
||||
statusMap = new Map<OrganizationUserStatusType, OrganizationUserUserDetailsResponse[]>();
|
||||
@@ -59,6 +61,10 @@ export class PeopleComponent implements OnInit {
|
||||
accessEvents = false;
|
||||
accessGroups = false;
|
||||
|
||||
protected didScroll = false;
|
||||
protected pageSize = 100;
|
||||
|
||||
private pagedUsersCount = 0;
|
||||
private modal: ModalComponent = null;
|
||||
private allUsers: OrganizationUserUserDetailsResponse[];
|
||||
|
||||
@@ -67,7 +73,7 @@ export class PeopleComponent implements OnInit {
|
||||
private platformUtilsService: PlatformUtilsService, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private cryptoService: CryptoService,
|
||||
private userService: UserService, private router: Router,
|
||||
private storageService: StorageService) { }
|
||||
private storageService: StorageService, private searchService: SearchService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
@@ -119,10 +125,27 @@ export class PeopleComponent implements OnInit {
|
||||
} else {
|
||||
this.users = this.allUsers;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
get allCount() {
|
||||
return this.allUsers.length;
|
||||
return this.allUsers != null ? this.allUsers.length : 0;
|
||||
}
|
||||
|
||||
get invitedCount() {
|
||||
@@ -294,6 +317,23 @@ export class PeopleComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
async resetPaging() {
|
||||
this.pagedUsers = [];
|
||||
this.loadMore();
|
||||
}
|
||||
|
||||
isSearching() {
|
||||
return this.searchService.isSearchable(this.searchText);
|
||||
}
|
||||
|
||||
isPaging() {
|
||||
const searching = this.isSearching();
|
||||
if (searching && this.didScroll) {
|
||||
this.resetPaging();
|
||||
}
|
||||
return !searching && this.users && this.users.length > this.pageSize;
|
||||
}
|
||||
|
||||
private async doConfirmation(user: OrganizationUserUserDetailsResponse) {
|
||||
const orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId);
|
||||
@@ -313,6 +353,7 @@ export class PeopleComponent implements OnInit {
|
||||
let index = this.users.indexOf(user);
|
||||
if (index > -1) {
|
||||
this.users.splice(index, 1);
|
||||
this.resetPaging();
|
||||
}
|
||||
if (this.statusMap.has(OrganizationUserStatusType.Accepted)) {
|
||||
index = this.statusMap.get(OrganizationUserStatusType.Accepted).indexOf(user);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
<app-callout [type]="'warning'">
|
||||
<p>{{'webPoliciesDeprecationWarning' | i18n}}</p>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
(click)="goToEnterprisePortal()">{{'businessPortal' | i18n}}</button>
|
||||
</app-callout>
|
||||
<div class="page-header d-flex">
|
||||
<h1>{{'policies' | i18n}}</h1>
|
||||
</div>
|
||||
@@ -8,7 +13,7 @@
|
||||
<table class="table table-hover table-list" *ngIf="!loading">
|
||||
<tbody>
|
||||
<tr *ngFor="let p of policies">
|
||||
<td>
|
||||
<td *ngIf="p.display">
|
||||
<a href="#" appStopClick (click)="edit(p)">{{p.name}}</a>
|
||||
<span class="badge badge-success" *ngIf="p.enabled">{{'enabled' | i18n}}</span>
|
||||
<small class="text-muted d-block">{{p.description}}</small>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { PolicyType } from 'jslib/enums/policyType';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
@@ -28,41 +29,25 @@ import { PolicyEditComponent } from './policy-edit.component';
|
||||
templateUrl: 'policies.component.html',
|
||||
})
|
||||
export class PoliciesComponent implements OnInit {
|
||||
@ViewChild('editTemplate', { read: ViewContainerRef }) editModalRef: ViewContainerRef;
|
||||
@ViewChild('editTemplate', { read: ViewContainerRef, static: true }) editModalRef: ViewContainerRef;
|
||||
|
||||
loading = true;
|
||||
organizationId: string;
|
||||
policies: any[];
|
||||
|
||||
// Remove when removing deprecation warning
|
||||
enterpriseTokenPromise: Promise<any>;
|
||||
private enterpriseUrl: string;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
private orgPolicies: PolicyResponse[];
|
||||
private policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute,
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private platformUtilsService: PlatformUtilsService, private userService: UserService,
|
||||
private router: Router) {
|
||||
this.policies = [
|
||||
{
|
||||
name: i18nService.t('twoStepLogin'),
|
||||
description: i18nService.t('twoStepLoginPolicyDesc'),
|
||||
type: PolicyType.TwoFactorAuthentication,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
name: i18nService.t('masterPass'),
|
||||
description: i18nService.t('masterPassPolicyDesc'),
|
||||
type: PolicyType.MasterPassword,
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
name: i18nService.t('passwordGenerator'),
|
||||
description: i18nService.t('passwordGeneratorPolicyDesc'),
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
private router: Router, private environmentService: EnvironmentService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
@@ -72,8 +57,53 @@ export class PoliciesComponent implements OnInit {
|
||||
this.router.navigate(['/organizations', this.organizationId]);
|
||||
return;
|
||||
}
|
||||
this.policies = [
|
||||
{
|
||||
name: this.i18nService.t('twoStepLogin'),
|
||||
description: this.i18nService.t('twoStepLoginPolicyDesc'),
|
||||
type: PolicyType.TwoFactorAuthentication,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('masterPass'),
|
||||
description: this.i18nService.t('masterPassPolicyDesc'),
|
||||
type: PolicyType.MasterPassword,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('passwordGenerator'),
|
||||
description: this.i18nService.t('passwordGeneratorPolicyDesc'),
|
||||
type: PolicyType.PasswordGenerator,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('singleOrg'),
|
||||
description: this.i18nService.t('singleOrgDesc'),
|
||||
type: PolicyType.SingleOrg,
|
||||
enabled: false,
|
||||
display: true,
|
||||
},
|
||||
{
|
||||
name: this.i18nService.t('requireSso'),
|
||||
description: this.i18nService.t('requireSsoPolicyDesc'),
|
||||
type: PolicyType.RequireSso,
|
||||
enabled: false,
|
||||
display: organization.useSso,
|
||||
},
|
||||
];
|
||||
await this.load();
|
||||
});
|
||||
|
||||
// Remove when removing deprecation warning
|
||||
this.enterpriseUrl = 'https://portal.bitwarden.com';
|
||||
if (this.environmentService.enterpriseUrl != null) {
|
||||
this.enterpriseUrl = this.environmentService.enterpriseUrl;
|
||||
} else if (this.environmentService.baseUrl != null) {
|
||||
this.enterpriseUrl = this.environmentService.baseUrl + '/portal';
|
||||
}
|
||||
}
|
||||
|
||||
async load() {
|
||||
@@ -102,6 +132,7 @@ export class PoliciesComponent implements OnInit {
|
||||
childComponent.description = p.description;
|
||||
childComponent.type = p.type;
|
||||
childComponent.organizationId = this.organizationId;
|
||||
childComponent.policiesEnabledMap = this.policiesEnabledMap;
|
||||
childComponent.onSavedPolicy.subscribe(() => {
|
||||
this.modal.close();
|
||||
this.load();
|
||||
@@ -111,4 +142,22 @@ export class PoliciesComponent implements OnInit {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Remove when removing deprecation warning
|
||||
async goToEnterprisePortal() {
|
||||
if (this.enterpriseTokenPromise != null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.enterpriseTokenPromise = this.apiService.getEnterprisePortalSignInToken();
|
||||
const token = await this.enterpriseTokenPromise;
|
||||
if (token != null) {
|
||||
const userId = await this.userService.getUserId();
|
||||
this.platformUtilsService.launchUri(this.enterpriseUrl + '/login?userId=' + userId +
|
||||
'&token=' + (window as any).encodeURIComponent(token) + '&organizationId=' + this.organizationId);
|
||||
}
|
||||
} catch { }
|
||||
this.enterpriseTokenPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,18 @@
|
||||
title="{{'warning' | i18n}}" icon="fa-warning">
|
||||
{{'twoStepLoginPolicyWarning' | i18n}}
|
||||
</app-callout>
|
||||
<app-callout type="warning" *ngIf="type === policyType.SingleOrg" title="{{'warning' | i18n}}"
|
||||
icon="fa-warning">
|
||||
{{'singleOrgPolicyWarning' | i18n}}
|
||||
</app-callout>
|
||||
<ng-container *ngIf="type === policyType.RequireSso">
|
||||
<app-callout type="tip" title="{{'prerequisite' | i18n}}">
|
||||
{{'requireSsoPolicyReq' | i18n}}
|
||||
</app-callout>
|
||||
<app-callout type="warning">
|
||||
{{'requireSsoExemption' | i18n}}
|
||||
</app-callout>
|
||||
</ng-container>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enabled" [(ngModel)]="enabled"
|
||||
|
||||
@@ -27,6 +27,7 @@ export class PolicyEditComponent implements OnInit {
|
||||
@Input() description: string;
|
||||
@Input() type: PolicyType;
|
||||
@Input() organizationId: string;
|
||||
@Input() policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
|
||||
@Output() onSavedPolicy = new EventEmitter();
|
||||
|
||||
policyType = PolicyType;
|
||||
@@ -127,45 +128,66 @@ export class PolicyEditComponent implements OnInit {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const request = new PolicyRequest();
|
||||
request.enabled = this.enabled;
|
||||
request.type = this.type;
|
||||
request.data = null;
|
||||
switch (this.type) {
|
||||
case PolicyType.PasswordGenerator:
|
||||
request.data = {
|
||||
defaultType: this.passGenDefaultType,
|
||||
minLength: this.passGenMinLength || null,
|
||||
useUpper: this.passGenUseUpper,
|
||||
useLower: this.passGenUseLower,
|
||||
useNumbers: this.passGenUseNumbers,
|
||||
useSpecial: this.passGenUseSpecial,
|
||||
minNumbers: this.passGenMinNumbers || null,
|
||||
minSpecial: this.passGenMinSpecial || null,
|
||||
minNumberWords: this.passGenMinNumberWords || null,
|
||||
capitalize: this.passGenCapitalize,
|
||||
includeNumber: this.passGenIncludeNumber,
|
||||
};
|
||||
break;
|
||||
case PolicyType.MasterPassword:
|
||||
request.data = {
|
||||
minComplexity: this.masterPassMinComplexity || null,
|
||||
minLength: this.masterPassMinLength || null,
|
||||
requireUpper: this.masterPassRequireUpper,
|
||||
requireLower: this.masterPassRequireLower,
|
||||
requireNumbers: this.masterPassRequireNumbers,
|
||||
requireSpecial: this.masterPassRequireSpecial,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (this.preValidate()) {
|
||||
const request = new PolicyRequest();
|
||||
request.enabled = this.enabled;
|
||||
request.type = this.type;
|
||||
request.data = null;
|
||||
switch (this.type) {
|
||||
case PolicyType.PasswordGenerator:
|
||||
request.data = {
|
||||
defaultType: this.passGenDefaultType,
|
||||
minLength: this.passGenMinLength || null,
|
||||
useUpper: this.passGenUseUpper,
|
||||
useLower: this.passGenUseLower,
|
||||
useNumbers: this.passGenUseNumbers,
|
||||
useSpecial: this.passGenUseSpecial,
|
||||
minNumbers: this.passGenMinNumbers || null,
|
||||
minSpecial: this.passGenMinSpecial || null,
|
||||
minNumberWords: this.passGenMinNumberWords || null,
|
||||
capitalize: this.passGenCapitalize,
|
||||
includeNumber: this.passGenIncludeNumber,
|
||||
};
|
||||
break;
|
||||
case PolicyType.MasterPassword:
|
||||
request.data = {
|
||||
minComplexity: this.masterPassMinComplexity || null,
|
||||
minLength: this.masterPassMinLength || null,
|
||||
requireUpper: this.masterPassRequireUpper,
|
||||
requireLower: this.masterPassRequireLower,
|
||||
requireNumbers: this.masterPassRequireNumbers,
|
||||
requireSpecial: this.masterPassRequireSpecial,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
try {
|
||||
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Edited Policy' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
|
||||
this.onSavedPolicy.emit();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private preValidate(): boolean {
|
||||
switch (this.type) {
|
||||
case PolicyType.RequireSso:
|
||||
if (!this.enabled) { // Don't need prevalidation checks if submitting to disable
|
||||
return true;
|
||||
}
|
||||
// Have SingleOrg policy enabled?
|
||||
if (!(this.policiesEnabledMap.has(PolicyType.SingleOrg)
|
||||
&& this.policiesEnabledMap.get(PolicyType.SingleOrg))) {
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('requireSsoPolicyReqError'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
this.formPromise = this.apiService.putPolicy(this.organizationId, this.type, request);
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Edited Policy' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('editedPolicyId', this.name));
|
||||
this.onSavedPolicy.emit();
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="userAddEditTitle">
|
||||
<div class="modal-dialog" [ngClass]="{'modal-lg': !editMode}" role="document">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="userAddEditTitle">
|
||||
@@ -19,11 +19,18 @@
|
||||
<p>{{'inviteUserDesc' | i18n}}</p>
|
||||
<div class="form-group mb-4">
|
||||
<label for="emails">{{'email' | i18n}}</label>
|
||||
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required>
|
||||
<input id="emails" class="form-control" type="text" name="Emails" [(ngModel)]="emails" required
|
||||
appAutoFocus>
|
||||
<small class="text-muted">{{'inviteMultipleEmailDesc' | i18n : '20'}}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h3>{{'userType' | i18n}}</h3>
|
||||
<h3>
|
||||
{{'userType' | i18n}}
|
||||
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||
href="https://bitwarden.com/help/article/user-types-access-control/#user-types">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</h3>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="userType" id="userTypeUser"
|
||||
[value]="organizationUserType.User" [(ngModel)]="type">
|
||||
@@ -59,6 +66,10 @@
|
||||
<h3 class="mt-4 d-flex">
|
||||
<div class="mb-2">
|
||||
{{'accessControl' | i18n}}
|
||||
<a target="_blank" rel="noopener" appA11yTitle="{{'learnMore' | i18n}}"
|
||||
href="https://bitwarden.com/help/article/user-types-access-control/#access-control">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
|
||||
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
|
||||
@@ -94,6 +105,7 @@
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th>{{'name' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'hidePasswords' | i18n}}</th>
|
||||
<th width="100" class="text-center">{{'readOnly' | i18n}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -106,6 +118,10 @@
|
||||
<td (click)="check(c)">
|
||||
{{c.name}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.hidePasswords"
|
||||
name="Collection[{{i}}].HidePasswords" [disabled]="!c.checked">
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<input type="checkbox" [(ngModel)]="c.readOnly" name="Collection[{{i}}].ReadOnly"
|
||||
[disabled]="!c.checked">
|
||||
|
||||
@@ -67,6 +67,7 @@ export class UserAddEditComponent implements OnInit {
|
||||
if (collection != null && collection.length > 0) {
|
||||
(collection[0] as any).checked = true;
|
||||
collection[0].readOnly = s.readOnly;
|
||||
collection[0].hidePasswords = s.hidePasswords;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -100,7 +101,7 @@ export class UserAddEditComponent implements OnInit {
|
||||
let collections: SelectionReadOnlyRequest[] = null;
|
||||
if (this.access !== 'all') {
|
||||
collections = this.collections.filter((c) => (c as any).checked)
|
||||
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly));
|
||||
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -10,17 +10,23 @@
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="name">{{'organizationName' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.name">
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="org.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)]="org.billingEmail">
|
||||
[(ngModel)]="org.billingEmail" [disabled]="selfHosted">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="businessName">{{'businessName' | i18n}}</label>
|
||||
<input id="businessName" class="form-control" type="text" name="BusinessName"
|
||||
[(ngModel)]="org.businessName">
|
||||
[(ngModel)]="org.businessName" [disabled]="selfHosted">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="identifier">{{'identifier' | i18n}}</label>
|
||||
<input id="identifier" class="form-control" type="text" name="Identifier"
|
||||
[(ngModel)]="org.identifier">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
@@ -49,9 +55,17 @@
|
||||
<h1>{{'taxInformation' | i18n}}</h1>
|
||||
</div>
|
||||
<p>{{'taxInformationDesc' | i18n}}</p>
|
||||
<a href="https://bitwarden.com/contact/" target="_blank" rel="noopener" class="btn btn-outline-secondary">
|
||||
{{'contactSupport' | i18n}}
|
||||
</a>
|
||||
<div *ngIf="!org || loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<form *ngIf="org && !loading" #formTax (ngSubmit)="submitTaxInfo()" [appApiAction]="taxFormPromise" ngNativeValidate>
|
||||
<app-tax-info></app-tax-info>
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="formTax.loading">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'save' | i18n}}</span>
|
||||
</button>
|
||||
</form>
|
||||
<div class="secondary-header text-danger border-0 mb-0">
|
||||
<h1>{{'dangerZone' | i18n}}</h1>
|
||||
</div>
|
||||
|
||||
@@ -11,31 +11,35 @@ import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { OrganizationUpdateRequest } from 'jslib/models/request/organizationUpdateRequest';
|
||||
import { OrganizationResponse } from 'jslib/models/response/organizationResponse';
|
||||
|
||||
import { ModalComponent } from '../../modal.component';
|
||||
import { ApiKeyComponent } from '../../settings/api-key.component';
|
||||
import { PurgeVaultComponent } from '../../settings/purge-vault.component';
|
||||
import { ApiKeyComponent } from './api-key.component';
|
||||
import { TaxInfoComponent } from '../../settings/tax-info.component';
|
||||
import { DeleteOrganizationComponent } from './delete-organization.component';
|
||||
import { RotateApiKeyComponent } from './rotate-api-key.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-org-account',
|
||||
templateUrl: 'account.component.html',
|
||||
})
|
||||
export class AccountComponent {
|
||||
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
|
||||
@ViewChild('apiKeyTemplate', { read: ViewContainerRef }) apiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef }) rotateApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('deleteOrganizationTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeOrganizationTemplate', { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef;
|
||||
@ViewChild('apiKeyTemplate', { read: ViewContainerRef, static: true }) apiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('rotateApiKeyTemplate', { read: ViewContainerRef, static: true }) rotateApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild(TaxInfoComponent) taxInfo: TaxInfoComponent;
|
||||
|
||||
selfHosted = false;
|
||||
loading = true;
|
||||
canUseApi = false;
|
||||
org: OrganizationResponse;
|
||||
formPromise: Promise<any>;
|
||||
taxFormPromise: Promise<any>;
|
||||
|
||||
private organizationId: string;
|
||||
private modal: ModalComponent = null;
|
||||
@@ -43,9 +47,11 @@ export class AccountComponent {
|
||||
constructor(private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private route: ActivatedRoute, private syncService: SyncService) { }
|
||||
private route: ActivatedRoute, private syncService: SyncService,
|
||||
private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
try {
|
||||
@@ -62,6 +68,7 @@ export class AccountComponent {
|
||||
request.name = this.org.name;
|
||||
request.businessName = this.org.businessName;
|
||||
request.billingEmail = this.org.billingEmail;
|
||||
request.identifier = this.org.identifier;
|
||||
this.formPromise = this.apiService.putOrganization(this.organizationId, request).then(() => {
|
||||
return this.syncService.fullSync(true);
|
||||
});
|
||||
@@ -71,6 +78,13 @@ export class AccountComponent {
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async submitTaxInfo() {
|
||||
this.taxFormPromise = this.taxInfo.submitTaxInfo();
|
||||
await this.taxFormPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Updated Organization Tax Info' });
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('taxInfoUpdated'));
|
||||
}
|
||||
|
||||
deleteOrganization() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
@@ -110,7 +124,14 @@ export class AccountComponent {
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.apiKeyModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.apiKeyModalRef);
|
||||
childComponent.organizationId = this.organizationId;
|
||||
childComponent.keyType = 'organization';
|
||||
childComponent.entityId = this.organizationId;
|
||||
childComponent.postKey = this.apiService.postOrganizationApiKey.bind(this.apiService);
|
||||
childComponent.scope = 'api.organization';
|
||||
childComponent.grantType = 'client_credentials';
|
||||
childComponent.apiKeyTitle = 'apiKey';
|
||||
childComponent.apiKeyWarning = 'apiKeyWarning';
|
||||
childComponent.apiKeyDescription = 'apiKeyDesc';
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
@@ -124,8 +145,16 @@ export class AccountComponent {
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.rotateApiKeyModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<RotateApiKeyComponent>(RotateApiKeyComponent, this.rotateApiKeyModalRef);
|
||||
childComponent.organizationId = this.organizationId;
|
||||
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.rotateApiKeyModalRef);
|
||||
childComponent.keyType = 'organization';
|
||||
childComponent.isRotation = true;
|
||||
childComponent.entityId = this.organizationId;
|
||||
childComponent.postKey = this.apiService.postOrganizationRotateApiKey.bind(this.apiService);
|
||||
childComponent.scope = 'api.organization';
|
||||
childComponent.grantType = 'client_credentials';
|
||||
childComponent.apiKeyTitle = 'apiKey';
|
||||
childComponent.apiKeyWarning = 'apiKeyWarning';
|
||||
childComponent.apiKeyDescription = 'apiKeyRotateDesc';
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
|
||||
@@ -33,7 +33,7 @@ export class AdjustSeatsComponent {
|
||||
@Output() onAdjusted = new EventEmitter<number>();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
|
||||
seatAdjustment = 0;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
aria-hidden="true">×</span></button>
|
||||
<h2 class="card-body-header">{{'changeBillingPlan' | i18n}}</h2>
|
||||
<p class="mb-0">{{'changeBillingPlanUpgrade' | i18n}}</p>
|
||||
<app-organization-plans [showFree]="false" [showCancel]="true" plan="families" [organizationId]="organizationId"
|
||||
(onCanceled)="cancel()">
|
||||
<app-organization-plans [showFree]="false" [showCancel]="true" [plan]="defaultUpgradePlan"
|
||||
[product]="defaultUpgradeProduct" [organizationId]="organizationId" (onCanceled)="cancel()">
|
||||
</app-organization-plans>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,9 @@ import {
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
import { ProductType } from 'jslib/enums/productType';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-plan',
|
||||
templateUrl: 'change-plan.component.html',
|
||||
@@ -18,6 +21,8 @@ export class ChangePlanComponent {
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
formPromise: Promise<any>;
|
||||
defaultUpgradePlan: PlanType = PlanType.FamiliesAnnually;
|
||||
defaultUpgradeProduct: ProductType = ProductType.Families;
|
||||
|
||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService) { }
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
</app-callout>
|
||||
<dl *ngIf="selfHosted">
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{sub.plan}}</dd>
|
||||
<dd>{{sub.plan.name}}</dd>
|
||||
<dt>{{'expiration' | i18n}}</dt>
|
||||
<dd *ngIf="sub.expiration">
|
||||
{{sub.expiration | date:'mediumDate'}}
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="col-4">
|
||||
<dl>
|
||||
<dt>{{'billingPlan' | i18n}}</dt>
|
||||
<dd>{{sub.plan}}</dd>
|
||||
<dd>{{sub.plan.name}}</dd>
|
||||
<ng-container *ngIf="subscription">
|
||||
<dt>{{'status' | i18n}}</dt>
|
||||
<dd>
|
||||
|
||||
@@ -13,7 +13,6 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { TokenService } from 'jslib/abstractions/token.service';
|
||||
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
|
||||
@@ -38,10 +37,10 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
cancelPromise: Promise<any>;
|
||||
reinstatePromise: Promise<any>;
|
||||
|
||||
constructor(private tokenService: TokenService, private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private messagingService: MessagingService, private route: ActivatedRoute) {
|
||||
constructor(private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private messagingService: MessagingService,
|
||||
private route: ActivatedRoute) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
@@ -192,34 +191,20 @@ export class OrganizationSubscriptionComponent implements OnInit {
|
||||
}
|
||||
|
||||
get billingInterval() {
|
||||
const monthly = this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.TeamsMonthly;
|
||||
const monthly = !this.sub.plan.isAnnual;
|
||||
return monthly ? 'month' : 'year';
|
||||
}
|
||||
|
||||
get storageGbPrice() {
|
||||
return this.billingInterval === 'month' ? 0.5 : 4;
|
||||
return this.sub.plan.additionalStoragePricePerGb;
|
||||
}
|
||||
|
||||
get seatPrice() {
|
||||
switch (this.sub.planType) {
|
||||
case PlanType.EnterpriseMonthly:
|
||||
return 4;
|
||||
case PlanType.EnterpriseAnnually:
|
||||
return 36;
|
||||
case PlanType.TeamsMonthly:
|
||||
return 2.5;
|
||||
case PlanType.TeamsAnnually:
|
||||
return 24;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
return this.sub.plan.seatPrice;
|
||||
}
|
||||
|
||||
get canAdjustSeats() {
|
||||
return this.sub.planType === PlanType.EnterpriseMonthly ||
|
||||
this.sub.planType === PlanType.EnterpriseAnnually ||
|
||||
this.sub.planType === PlanType.TeamsMonthly || this.sub.planType === PlanType.TeamsAnnually;
|
||||
return this.sub.plan.hasAdditionalSeatsOption;
|
||||
}
|
||||
|
||||
get canDownloadLicense() {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="rotateKeyTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="rotateKeyTitle">{{'rotateApiKey' | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{'apiKeyRotateDesc' | i18n}}</p>
|
||||
<ng-container *ngIf="!clientSecret">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
</ng-container>
|
||||
<app-callout type="warning" *ngIf="clientSecret">{{'apiKeyWarning' | i18n}}</app-callout>
|
||||
<app-callout type="info" title="{{'oauth2ClientCredentials' | i18n}}" icon="fa-key"
|
||||
*ngIf="clientSecret">
|
||||
<p class="mb-1">
|
||||
<strong>client_id:</strong><br>
|
||||
<code>{{clientId}}</code>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>client_secret:</strong><br>
|
||||
<code>{{clientSecret}}</code>
|
||||
</p>
|
||||
<p class="mb-1">
|
||||
<strong>scope:</strong><br>
|
||||
<code>{{scope}}</code>
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<strong>grant_type:</strong><br>
|
||||
<code>client_credentials</code>
|
||||
</p>
|
||||
</app-callout>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"
|
||||
*ngIf="!clientSecret">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'rotateApiKey' | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest';
|
||||
|
||||
import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
|
||||
|
||||
@Component({
|
||||
selector: 'app-rotate-api-key',
|
||||
templateUrl: 'rotate-api-key.component.html',
|
||||
})
|
||||
export class RotateApiKeyComponent {
|
||||
organizationId: string;
|
||||
|
||||
masterPassword: string;
|
||||
formPromise: Promise<ApiKeyResponse>;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scope: string;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private router: Router) { }
|
||||
|
||||
async submit() {
|
||||
if (this.masterPassword == null || this.masterPassword === '') {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = new PasswordVerificationRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
|
||||
try {
|
||||
this.formPromise = this.apiService.postOrganizationRotateApiKey(this.organizationId, request);
|
||||
const response = await this.formPromise;
|
||||
this.clientSecret = response.apiKey;
|
||||
this.clientId = 'organization.' + this.organizationId;
|
||||
this.scope = 'api.organization';
|
||||
this.analytics.eventTrack.next({ action: 'Rotated Organization API Key' });
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,7 @@ export class AddEditComponent extends BaseAddEditComponent {
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.deleteCipher();
|
||||
}
|
||||
return this.apiService.deleteCipherAdmin(this.cipherId);
|
||||
return this.cipher.isDeleted ? this.apiService.deleteCipherAdmin(this.cipherId)
|
||||
: this.apiService.putDeleteCipherAdmin(this.cipherId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
|
||||
async load(filter: (cipher: CipherView) => boolean = null) {
|
||||
if (!this.organization.isAdmin) {
|
||||
await super.load(filter);
|
||||
await super.load(filter, this.deleted);
|
||||
return;
|
||||
}
|
||||
this.accessEvents = this.organization.useEvents;
|
||||
@@ -65,30 +65,32 @@ export class CiphersComponent extends BaseCiphersComponent {
|
||||
}
|
||||
this.searchPending = false;
|
||||
let filteredCiphers = this.allCiphers;
|
||||
if (this.filter != null) {
|
||||
filteredCiphers = filteredCiphers.filter(this.filter);
|
||||
}
|
||||
|
||||
if (this.searchText == null || this.searchText.trim().length < 2) {
|
||||
this.ciphers = filteredCiphers;
|
||||
this.ciphers = filteredCiphers.filter((c) => {
|
||||
if (c.isDeleted !== this.deleted) {
|
||||
return false;
|
||||
}
|
||||
return this.filter == null || this.filter(c);
|
||||
});
|
||||
} else {
|
||||
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText);
|
||||
if (this.filter != null) {
|
||||
filteredCiphers = filteredCiphers.filter(this.filter);
|
||||
}
|
||||
this.ciphers = this.searchService.searchCiphersBasic(filteredCiphers, this.searchText, this.deleted);
|
||||
}
|
||||
await this.resetPaging();
|
||||
}
|
||||
|
||||
checkCipher(c: CipherView) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
events(c: CipherView) {
|
||||
this.onEventsClicked.emit(c);
|
||||
}
|
||||
|
||||
protected deleteCipher(id: string) {
|
||||
if (!this.organization.isAdmin) {
|
||||
return super.deleteCipher(id);
|
||||
return super.deleteCipher(id, this.deleted);
|
||||
}
|
||||
return this.apiService.deleteCipherAdmin(id);
|
||||
return this.deleted ? this.apiService.deleteCipherAdmin(id) : this.apiService.putDeleteCipherAdmin(id);
|
||||
}
|
||||
|
||||
protected showFixOldAttachments(c: CipherView) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<app-org-vault-groupings [showFolders]="false" [showFavorites]="false"
|
||||
<app-org-vault-groupings [showFolders]="false" [showFavorites]="false" [showTrash]="true"
|
||||
(onAllClicked)="clearGroupingFilters()" (onCipherTypeClicked)="filterCipherType($event)"
|
||||
(onCollectionClicked)="filterCollection($event.id)" (onSearchTextChanged)="filterSearchText($event)">
|
||||
(onCollectionClicked)="filterCollection($event.id)" (onSearchTextChanged)="filterSearchText($event)"
|
||||
(onTrashClicked)="filterDeleted()">
|
||||
</app-org-vault-groupings>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
@@ -18,9 +19,15 @@
|
||||
</ng-container>
|
||||
</small>
|
||||
</h1>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
|
||||
</button>
|
||||
<div class="ml-auto d-flex">
|
||||
<app-vault-bulk-actions [ciphersComponent]="ciphersComponent" [modal]="modal" [deleted]="deleted"
|
||||
[organization]="organization">
|
||||
</app-vault-bulk-actions>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm ml-auto" (click)="addCipher()"
|
||||
*ngIf="!deleted">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'addItem' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<app-org-vault-ciphers (onCipherClicked)="editCipher($event)"
|
||||
(onAttachmentsClicked)="editCipherAttachments($event)" (onAddCipher)="addCipher()"
|
||||
@@ -33,4 +40,4 @@
|
||||
<ng-template #attachments></ng-template>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
<ng-template #collections></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
<ng-template #eventsTemplate></ng-template>
|
||||
|
||||
@@ -41,18 +41,19 @@ const BroadcasterSubscriptionId = 'OrgVaultComponent';
|
||||
templateUrl: 'vault.component.html',
|
||||
})
|
||||
export class VaultComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent;
|
||||
@ViewChild(CiphersComponent) ciphersComponent: CiphersComponent;
|
||||
@ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef;
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild('collections', { read: ViewContainerRef }) collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef }) eventsModalRef: ViewContainerRef;
|
||||
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
|
||||
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
|
||||
@ViewChild('attachments', { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef;
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild('collections', { read: ViewContainerRef, static: true }) collectionsModalRef: ViewContainerRef;
|
||||
@ViewChild('eventsTemplate', { read: ViewContainerRef, static: true }) eventsModalRef: ViewContainerRef;
|
||||
|
||||
organization: Organization;
|
||||
collectionId: string;
|
||||
type: CipherType;
|
||||
collectionId: string = null;
|
||||
type: CipherType = null;
|
||||
deleted: boolean = false;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
modal: ModalComponent = null;
|
||||
|
||||
constructor(private route: ActivatedRoute, private userService: UserService,
|
||||
private router: Router, private changeDetectorRef: ChangeDetectorRef,
|
||||
@@ -61,7 +62,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private broadcasterService: BroadcasterService, private ngZone: NgZone) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.route.parent.params.subscribe(async (params) => {
|
||||
const queryParams = this.route.parent.params.subscribe(async (params) => {
|
||||
this.organization = await this.userService.getOrganization(params.organizationId);
|
||||
this.groupingsComponent.organization = this.organization;
|
||||
this.ciphersComponent.organization = this.organization;
|
||||
@@ -92,7 +93,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.groupingsComponent.selectedAll = true;
|
||||
await this.ciphersComponent.reload();
|
||||
} else {
|
||||
if (qParams.type) {
|
||||
if (qParams.deleted) {
|
||||
this.groupingsComponent.selectedTrash = true;
|
||||
await this.filterDeleted(true);
|
||||
} else if (qParams.type) {
|
||||
const t = parseInt(qParams.type, null);
|
||||
this.groupingsComponent.selectedType = t;
|
||||
await this.filterCipherType(t, true);
|
||||
@@ -116,6 +120,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
queryParamsSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
|
||||
if (queryParams != null) {
|
||||
queryParams.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -125,6 +133,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
async clearGroupingFilters() {
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.ciphersComponent.deleted = false;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault');
|
||||
await this.ciphersComponent.applyFilter();
|
||||
this.clearFilters();
|
||||
@@ -133,6 +142,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
async filterCipherType(type: CipherType, load = false) {
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.ciphersComponent.deleted = false;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchType');
|
||||
const filter = (c: CipherView) => c.type === type;
|
||||
if (load) {
|
||||
@@ -147,6 +157,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
async filterCollection(collectionId: string, load = false) {
|
||||
this.ciphersComponent.showAddNew = true;
|
||||
this.ciphersComponent.deleted = false;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchCollection');
|
||||
const filter = (c: CipherView) => {
|
||||
if (collectionId === 'unassigned') {
|
||||
@@ -165,6 +176,20 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.go();
|
||||
}
|
||||
|
||||
async filterDeleted(load: boolean = false) {
|
||||
this.ciphersComponent.showAddNew = false;
|
||||
this.ciphersComponent.deleted = true;
|
||||
this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchTrash');
|
||||
if (load) {
|
||||
await this.ciphersComponent.reload(null, true);
|
||||
} else {
|
||||
await this.ciphersComponent.applyFilter(null);
|
||||
}
|
||||
this.clearFilters();
|
||||
this.deleted = true;
|
||||
this.go();
|
||||
}
|
||||
|
||||
filterSearchText(searchText: string) {
|
||||
this.ciphersComponent.searchText = searchText;
|
||||
this.ciphersComponent.search(200);
|
||||
@@ -255,6 +280,10 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
childComponent.onRestoredCipher.subscribe(async (c: CipherView) => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
@@ -299,6 +328,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private clearFilters() {
|
||||
this.collectionId = null;
|
||||
this.type = null;
|
||||
this.deleted = false;
|
||||
}
|
||||
|
||||
private go(queryParams: any = null) {
|
||||
@@ -306,6 +336,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
queryParams = {
|
||||
type: this.type,
|
||||
collectionId: this.collectionId,
|
||||
deleted: this.deleted ? true : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
59
src/app/send/access.component.html
Normal file
59
src/app/send/access.component.html
Normal file
@@ -0,0 +1,59 @@
|
||||
<form #form (ngSubmit)="load()" [appApiAction]="formPromise" class="container" ngNativeValidate>
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">Bitwarden Send</p>
|
||||
<div class="card d-block">
|
||||
<div class="card-body" *ngIf="loading" class="text-center">
|
||||
<i class="fa fa-spinner fa-spin fa-2x text-muted" title="{{'loading' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!loading && passwordRequired">
|
||||
<p>{{'sendProtectedPassword' | i18n}}</p>
|
||||
<p>{{'sendProtectedPasswordDontKnow' | i18n}}</p>
|
||||
<div class="form-group">
|
||||
<label for="password">{{'password' | i18n}}</label>
|
||||
<input id="password" type="password" name="Password" class="text-monospace form-control"
|
||||
[(ngModel)]="password" required appInputVerbatim appAutofocus>
|
||||
</div>
|
||||
<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> {{'continue' | i18n}}
|
||||
</span>
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body" *ngIf="!loading && !passwordRequired && send">
|
||||
<p class="text-center"><b>{{send.name}}</b></p>
|
||||
<hr>
|
||||
<!-- Text -->
|
||||
<ng-container *ngIf="send.type === sendType.Text">
|
||||
<app-callout *ngIf="send.text.hidden" type="tip">{{'sendHiddenByDefault' | i18n}}</app-callout>
|
||||
<div class="form-group">
|
||||
<textarea id="text" rows="8" name="Text" [(ngModel)]="sendText" class="form-control"
|
||||
readonly (click)="selectText()"></textarea>
|
||||
</div>
|
||||
<button class="btn btn-block btn-link" type="button" (click)="toggleText()"
|
||||
*ngIf="send.text.hidden">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showText, 'fa-eye-slash': showText}"></i>
|
||||
{{'toggleVisibility' | i18n}}
|
||||
</button>
|
||||
<button class="btn btn-block btn-link" type="button" (click)="copyText()">
|
||||
<i class="fa fa-copy" aria-hidden="true"></i> {{'copyValue' | i18n}}
|
||||
</button>
|
||||
</ng-container>
|
||||
<!-- File -->
|
||||
<ng-container *ngIf="send.type === sendType.File">
|
||||
<p>{{send.file.fileName}}</p>
|
||||
<button class="btn btn-primary btn-block" type="button" (click)="download()">
|
||||
<i class="fa fa-download" aria-hidden="true"></i>
|
||||
{{'downloadFile' | i18n}} ({{send.file.sizeName}})</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
139
src/app/send/access.component.ts
Normal file
139
src/app/send/access.component.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
import { SendAccess } from 'jslib/models/domain/sendAccess';
|
||||
|
||||
import { SendAccessView } from 'jslib/models/view/sendAccessView';
|
||||
|
||||
import { SendType } from 'jslib/enums/sendType';
|
||||
import { SendAccessRequest } from 'jslib/models/request/sendAccessRequest';
|
||||
import { ErrorResponse } from 'jslib/models/response/errorResponse';
|
||||
|
||||
import { SendAccessResponse } from 'jslib/models/response/sendAccessResponse';
|
||||
|
||||
@Component({
|
||||
selector: 'app-send-access',
|
||||
templateUrl: 'access.component.html',
|
||||
})
|
||||
export class AccessComponent implements OnInit {
|
||||
send: SendAccessView;
|
||||
sendType = SendType;
|
||||
downloading = false;
|
||||
loading = true;
|
||||
passwordRequired = false;
|
||||
formPromise: Promise<SendAccessResponse>;
|
||||
password: string;
|
||||
showText = false;
|
||||
|
||||
private id: string;
|
||||
private key: string;
|
||||
private decKey: SymmetricCryptoKey;
|
||||
|
||||
constructor(private i18nService: I18nService, private cryptoFunctionService: CryptoFunctionService,
|
||||
private apiService: ApiService, private platformUtilsService: PlatformUtilsService,
|
||||
private route: ActivatedRoute, private cryptoService: CryptoService) {
|
||||
}
|
||||
|
||||
get sendText() {
|
||||
if (this.send == null || this.send.text == null) {
|
||||
return null;
|
||||
}
|
||||
return this.showText ? this.send.text.text : this.send.text.maskedText;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.params.subscribe(async (params) => {
|
||||
this.id = params.sendId;
|
||||
this.key = params.key;
|
||||
if (this.key == null || this.id == null) {
|
||||
return;
|
||||
}
|
||||
await this.load();
|
||||
});
|
||||
}
|
||||
|
||||
async download() {
|
||||
if (this.send == null || this.decKey == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.downloading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.downloading = true;
|
||||
const response = await fetch(new Request(this.send.file.url, { cache: 'no-store' }));
|
||||
if (response.status !== 200) {
|
||||
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
|
||||
this.downloading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const buf = await response.arrayBuffer();
|
||||
const decBuf = await this.cryptoService.decryptFromBytes(buf, this.decKey);
|
||||
this.platformUtilsService.saveFile(window, decBuf, null, this.send.file.fileName);
|
||||
} catch (e) {
|
||||
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
|
||||
}
|
||||
|
||||
this.downloading = false;
|
||||
}
|
||||
|
||||
selectText() {
|
||||
(document.getElementById('text') as HTMLInputElement).select();
|
||||
}
|
||||
|
||||
copyText() {
|
||||
this.platformUtilsService.copyToClipboard(this.send.text.text);
|
||||
this.platformUtilsService.showToast('success', null,
|
||||
this.i18nService.t('valueCopied', this.i18nService.t('sendTypeText')));
|
||||
}
|
||||
|
||||
toggleText() {
|
||||
this.showText = !this.showText;
|
||||
}
|
||||
|
||||
async load() {
|
||||
const keyArray = Utils.fromUrlB64ToArray(this.key);
|
||||
const accessRequest = new SendAccessRequest();
|
||||
if (this.password != null) {
|
||||
const passwordHash = await this.cryptoFunctionService.pbkdf2(this.password, keyArray, 'sha256', 100000);
|
||||
accessRequest.password = Utils.fromBufferToB64(passwordHash);
|
||||
}
|
||||
try {
|
||||
let sendResponse: SendAccessResponse = null;
|
||||
if (this.loading) {
|
||||
sendResponse = await this.apiService.postSendAccess(this.id, accessRequest);
|
||||
} else {
|
||||
this.formPromise = this.apiService.postSendAccess(this.id, accessRequest);
|
||||
sendResponse = await this.formPromise;
|
||||
}
|
||||
this.passwordRequired = false;
|
||||
const sendAccess = new SendAccess(sendResponse);
|
||||
this.decKey = await this.cryptoService.makeSendKey(keyArray);
|
||||
this.send = await sendAccess.decrypt(this.decKey);
|
||||
this.showText = this.send.text != null ? !this.send.text.hidden : true;
|
||||
} catch (e) {
|
||||
if (e instanceof ErrorResponse) {
|
||||
if (e.statusCode === 401) {
|
||||
this.passwordRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
129
src/app/send/add-edit.component.html
Normal file
129
src/app/send/add-edit.component.html
Normal file
@@ -0,0 +1,129 @@
|
||||
<div class="modal fade" tabindex="-1" role="dialog" aria-modal="true" aria-labelledby="sendAddEditTitle">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate
|
||||
autocomplete="off">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="sendAddEditTitle">{{title}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" *ngIf="send">
|
||||
<div class="row" *ngIf="!editMode">
|
||||
<div class="col-6 form-group">
|
||||
<label for="type">{{'whatTypeOfSend' | i18n}}</label>
|
||||
<select id="type" name="Type" [(ngModel)]="send.type" class="form-control" appAutofocus>
|
||||
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="send.name" required>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Text -->
|
||||
<ng-container *ngIf="send.type === sendType.Text">
|
||||
<div class="form-group">
|
||||
<label for="text">{{'sendTypeText' | i18n}}</label>
|
||||
<textarea id="text" name="Text.Text" rows="6" [(ngModel)]="send.text.text"
|
||||
class="form-control"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" [(ngModel)]="send.text.hidden"
|
||||
id="text-hidden" name="Text.Hidden">
|
||||
<label class="form-check-label" for="text-hidden">{{'cfTypeHidden' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<!-- File -->
|
||||
<ng-container *ngIf="send.type === sendType.File">
|
||||
<div class="form-group">
|
||||
<div *ngIf="editMode">
|
||||
<strong class="d-block">{{'file' | i18n}}</strong>
|
||||
{{send.file.fileName}} ({{send.file.sizeName}})
|
||||
</div>
|
||||
<div *ngIf="!editMode">
|
||||
<label for="file">{{'file' | i18n}}</label>
|
||||
<input type="file" id="file" class="form-control-file" name="file" required>
|
||||
<small class="form-text text-muted">{{'maxFileSize' | i18n}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h3 class="mt-4">{{'options' | i18n}}</h3>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="deletionDate">{{'deletionDate' | i18n}}</label>
|
||||
<input id="deletionDate" class="form-control" type="datetime-local" name="DeletionDate"
|
||||
[(ngModel)]="deletionDate" required>
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<div class="d-flex">
|
||||
<label for="expirationDate">{{'expirationDate' | i18n}}</label>
|
||||
<a href="#" appStopClick (click)="clearExpiration()" class="ml-auto">
|
||||
{{'clear' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
<input id="expirationDate" class="form-control" type="datetime-local" name="ExpirationDate"
|
||||
[(ngModel)]="expirationDate">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="maxAccessCount">{{'maxAccessCount' | i18n}}</label>
|
||||
<input id="maxAccessCount" class="form-control" type="number" name="MaxAccessCount"
|
||||
[(ngModel)]="send.maxAccessCount">
|
||||
</div>
|
||||
<div class="col-6 form-group" *ngIf="editMode">
|
||||
<label for="accessCount">{{'currentAccessCount' | i18n}}</label>
|
||||
<input id="accessCount" class="form-control" type="number" name="AccessCount" readonly
|
||||
[(ngModel)]="send.accessCount">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="password" *ngIf="!hasPassword">{{'password' | i18n}}</label>
|
||||
<label for="password" *ngIf="hasPassword">{{'newPassword' | i18n}}</label>
|
||||
<input id="password" class="form-control" type="password" name="Password"
|
||||
[(ngModel)]="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="notes">{{'notes' | i18n}}</label>
|
||||
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="send.notes" class="form-control"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" [(ngModel)]="send.disabled" id="disabled"
|
||||
name="Disabled">
|
||||
<label class="form-check-label" for="disabled">{{'disabled' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="link">
|
||||
<label for="link">{{'sendLink' | i18n}}</label>
|
||||
<input type="text" readonly id="link" name="Link" [(ngModel)]="link" class="form-control">
|
||||
</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" *ngIf="send">
|
||||
<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>
|
||||
276
src/app/send/add-edit.component.ts
Normal file
276
src/app/send/add-edit.component.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
|
||||
import {
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { SendType } from 'jslib/enums/sendType';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
|
||||
import { SendView } from 'jslib/models/view/sendView';
|
||||
import { SendFileView } from 'jslib/models/view/sendFileView';
|
||||
import { SendTextView } from 'jslib/models/view/sendTextView';
|
||||
|
||||
import { Send } from 'jslib/models/domain/send';
|
||||
import { SendFile } from 'jslib/models/domain/sendFile';
|
||||
import { SendText } from 'jslib/models/domain/sendText';
|
||||
|
||||
import { SendData } from 'jslib/models/data/sendData';
|
||||
|
||||
import { SendRequest } from 'jslib/models/request/sendRequest';
|
||||
|
||||
import { Utils } from 'jslib/misc/utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-send-add-edit',
|
||||
templateUrl: 'add-edit.component.html',
|
||||
})
|
||||
export class AddEditComponent {
|
||||
@Input() sendId: string;
|
||||
@Input() type: SendType;
|
||||
|
||||
@Output() onSavedSend = new EventEmitter<SendView>();
|
||||
@Output() onDeletedSend = new EventEmitter<SendView>();
|
||||
@Output() onCancelled = new EventEmitter<SendView>();
|
||||
|
||||
editMode: boolean = false;
|
||||
send: SendView;
|
||||
link: string;
|
||||
title: string;
|
||||
deletionDate: string;
|
||||
expirationDate: string;
|
||||
hasPassword: boolean;
|
||||
password: string;
|
||||
formPromise: Promise<any>;
|
||||
deletePromise: Promise<any>;
|
||||
sendType = SendType;
|
||||
typeOptions: any[];
|
||||
|
||||
constructor(private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
|
||||
private apiService: ApiService, private cryptoService: CryptoService,
|
||||
private cryptoFunctionService: CryptoFunctionService, private environmentService: EnvironmentService,
|
||||
private datePipe: DatePipe) {
|
||||
this.typeOptions = [
|
||||
{ name: i18nService.t('sendTypeFile'), value: SendType.File },
|
||||
{ name: i18nService.t('sendTypeText'), value: SendType.Text },
|
||||
];
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.editMode = this.sendId != null;
|
||||
if (this.editMode) {
|
||||
this.editMode = true;
|
||||
this.title = this.i18nService.t('editSend');
|
||||
} else {
|
||||
this.title = this.i18nService.t('createSend');
|
||||
}
|
||||
|
||||
if (this.send == null) {
|
||||
if (this.editMode) {
|
||||
const send = await this.loadSend();
|
||||
this.send = await send.decrypt();
|
||||
} else {
|
||||
this.send = new SendView();
|
||||
this.send.type = this.type == null ? SendType.File : this.type;
|
||||
this.send.file = new SendFileView();
|
||||
this.send.text = new SendTextView();
|
||||
this.send.deletionDate = new Date();
|
||||
this.send.deletionDate.setDate(this.send.deletionDate.getDate() + 7);
|
||||
}
|
||||
}
|
||||
|
||||
this.hasPassword = this.send.password != null && this.send.password.trim() !== '';
|
||||
|
||||
// Parse dates
|
||||
this.deletionDate = this.send.deletionDate == null ? null :
|
||||
this.datePipe.transform(this.send.deletionDate, 'yyyy-MM-ddTHH:mm');
|
||||
this.expirationDate = this.send.expirationDate == null ? null :
|
||||
this.datePipe.transform(this.send.expirationDate, 'yyyy-MM-ddTHH:mm');
|
||||
|
||||
if (this.editMode) {
|
||||
let webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
if (webVaultUrl == null) {
|
||||
webVaultUrl = 'https://vault.bitwarden.com';
|
||||
}
|
||||
this.link = webVaultUrl + '/#/send/' + this.send.accessId + '/' + this.send.urlB64Key;
|
||||
}
|
||||
}
|
||||
|
||||
async submit(): Promise<boolean> {
|
||||
if (this.send.name == null || this.send.name === '') {
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nameRequired'));
|
||||
return false;
|
||||
}
|
||||
|
||||
let file: File = null;
|
||||
if (this.send.type === SendType.File && !this.editMode) {
|
||||
const fileEl = document.getElementById('file') as HTMLInputElement;
|
||||
const files = fileEl.files;
|
||||
if (files == null || files.length === 0) {
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('selectFile'));
|
||||
return;
|
||||
}
|
||||
|
||||
file = files[0];
|
||||
if (file.size > 104857600) { // 100 MB
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('maxFileSize'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const encSend = await this.encryptSend(file);
|
||||
try {
|
||||
this.formPromise = this.saveSend(encSend);
|
||||
await this.formPromise;
|
||||
this.send.id = encSend[0].id;
|
||||
this.platformUtilsService.showToast('success', null,
|
||||
this.i18nService.t(this.editMode ? 'editedSend' : 'createdSend'));
|
||||
this.onSavedSend.emit(this.send);
|
||||
return true;
|
||||
} catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
clearExpiration() {
|
||||
this.expirationDate = null;
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
if (this.deletePromise != null) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('deleteSendConfirmation'),
|
||||
this.i18nService.t('deleteSend'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.deletePromise = this.apiService.deleteSend(this.send.id);
|
||||
await this.deletePromise;
|
||||
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
|
||||
await this.load();
|
||||
this.onDeletedSend.emit(this.send);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
protected async loadSend(): Promise<Send> {
|
||||
const response = await this.apiService.getSend(this.sendId);
|
||||
const data = new SendData(response);
|
||||
return new Send(data);
|
||||
}
|
||||
|
||||
protected async encryptSend(file: File): Promise<[Send, ArrayBuffer]> {
|
||||
let fileData: ArrayBuffer = null;
|
||||
const send = new Send();
|
||||
send.id = this.send.id;
|
||||
send.type = this.send.type;
|
||||
send.disabled = this.send.disabled;
|
||||
send.maxAccessCount = this.send.maxAccessCount;
|
||||
if (this.send.key == null) {
|
||||
this.send.key = await this.cryptoFunctionService.randomBytes(16);
|
||||
this.send.cryptoKey = await this.cryptoService.makeSendKey(this.send.key);
|
||||
}
|
||||
if (this.password != null) {
|
||||
const passwordHash = await this.cryptoFunctionService.pbkdf2(this.password,
|
||||
this.send.key, 'sha256', 100000);
|
||||
send.password = Utils.fromBufferToB64(passwordHash);
|
||||
}
|
||||
send.key = await this.cryptoService.encrypt(this.send.key, null);
|
||||
send.name = await this.cryptoService.encrypt(this.send.name, this.send.cryptoKey);
|
||||
send.notes = await this.cryptoService.encrypt(this.send.notes, this.send.cryptoKey);
|
||||
if (send.type === SendType.Text) {
|
||||
send.text = new SendText();
|
||||
send.text.text = await this.cryptoService.encrypt(this.send.text.text, this.send.cryptoKey);
|
||||
send.text.hidden = this.send.text.hidden;
|
||||
} else if (send.type === SendType.File) {
|
||||
send.file = new SendFile();
|
||||
if (file != null) {
|
||||
fileData = await this.parseFile(send, file);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse dates
|
||||
try {
|
||||
send.deletionDate = this.deletionDate == null ? null : new Date(this.deletionDate);
|
||||
} catch {
|
||||
send.deletionDate = null;
|
||||
}
|
||||
try {
|
||||
send.expirationDate = this.expirationDate == null ? null : new Date(this.expirationDate);
|
||||
} catch {
|
||||
send.expirationDate = null;
|
||||
}
|
||||
|
||||
return [send, fileData];
|
||||
}
|
||||
|
||||
protected async saveSend(sendData: [Send, ArrayBuffer]) {
|
||||
const request = new SendRequest(sendData[0]);
|
||||
if (sendData[0].id == null) {
|
||||
if (sendData[0].type === SendType.Text) {
|
||||
await this.apiService.postSend(request);
|
||||
} else {
|
||||
const fd = new FormData();
|
||||
try {
|
||||
const blob = new Blob([sendData[1]], { type: 'application/octet-stream' });
|
||||
fd.append('model', JSON.stringify(request));
|
||||
fd.append('data', blob, sendData[0].file.fileName.encryptedString);
|
||||
} catch (e) {
|
||||
if (Utils.isNode && !Utils.isBrowser) {
|
||||
fd.append('model', JSON.stringify(request));
|
||||
fd.append('data', Buffer.from(sendData[1]) as any, {
|
||||
filepath: sendData[0].file.fileName.encryptedString,
|
||||
contentType: 'application/octet-stream',
|
||||
} as any);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await this.apiService.postSendFile(fd);
|
||||
}
|
||||
} else {
|
||||
await this.apiService.putSend(sendData[0].id, request);
|
||||
}
|
||||
}
|
||||
|
||||
private parseFile(send: Send, file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsArrayBuffer(file);
|
||||
reader.onload = async (evt) => {
|
||||
try {
|
||||
send.file.fileName = await this.cryptoService.encrypt(file.name, this.send.cryptoKey);
|
||||
const fileData = await this.cryptoService.encryptToBytes(evt.target.result as ArrayBuffer,
|
||||
this.send.cryptoKey);
|
||||
resolve(fileData);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
};
|
||||
reader.onerror = (evt) => {
|
||||
reject('Error reading file.');
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
113
src/app/send/send.component.html
Normal file
113
src/app/send/send.component.html
Normal file
@@ -0,0 +1,113 @@
|
||||
<div class="container page-content">
|
||||
<div class="row">
|
||||
<div class="col-3 groupings">
|
||||
<div class="card vault-filters">
|
||||
<div class="card-header d-flex">
|
||||
{{'filters' | i18n}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<input type="search" placeholder="{{searchPlaceholder || ('searchSends' | i18n)}}" id="search"
|
||||
class="form-control" [(ngModel)]="searchText" (input)="searchTextChanged()" autocomplete="off"
|
||||
appAutofocus>
|
||||
<ul class="fa-ul card-ul">
|
||||
<li [ngClass]="{active: selectedAll}">
|
||||
<a href="#" appStopClick (click)="selectAll()">
|
||||
<i class="fa-li fa fa-fw fa-th"></i>{{'allSends' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{'types' | i18n}}</h3>
|
||||
<ul class="fa-ul card-ul">
|
||||
<li [ngClass]="{active: selectedType === sendType.Text}">
|
||||
<a href="#" appStopClick (click)="selectType(sendType.Text)">
|
||||
<i class="fa-li fa fa-fw fa-file-text-o"></i>{{'sendTypeText' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
<li [ngClass]="{active: selectedType === sendType.File}">
|
||||
<a href="#" appStopClick (click)="selectType(sendType.File)">
|
||||
<i class="fa-li fa fa-fw fa-file-o"></i>{{'sendTypeFile' | i18n}}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<div class="page-header d-flex">
|
||||
<h1>
|
||||
Send
|
||||
<small #actionSpinner [appApiAction]="actionPromise">
|
||||
<ng-container *ngIf="actionSpinner.loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}"
|
||||
aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
</small>
|
||||
</h1>
|
||||
<div class="ml-auto d-flex">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" (click)="addSend()">
|
||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>{{'createSend' | i18n}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!--Listing Table-->
|
||||
<table class="table table-hover table-list" *ngIf="filteredSends && filteredSends.length">
|
||||
<tbody>
|
||||
<tr *ngFor="let s of filteredSends">
|
||||
<td class="table-list-icon">
|
||||
<div class="icon" aria-hidden="true">
|
||||
<i class="fa fa-fw fa-lg fa-file-o" *ngIf="s.type == sendType.File"></i>
|
||||
<i class="fa fa-fw fa-lg fa-file-text-o" *ngIf="s.type == sendType.Text"></i>
|
||||
</div>
|
||||
</td>
|
||||
<td class="reduced-lh wrap">
|
||||
<a href="#" appStopClick appStopProp (click)="editSend(s)">{{s.name}}</a>
|
||||
<ng-container *ngIf="s.password">
|
||||
<i class="fa fa-key" appStopProp title="{{'password' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'password' | i18n}}</span>
|
||||
</ng-container>
|
||||
<br>
|
||||
<small appStopProp>{{s.deletionDate | date:'medium'}}</small>
|
||||
</td>
|
||||
<td class="table-list-options">
|
||||
<div class="dropdown" appListDropdown>
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button"
|
||||
id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="false" appA11yTitle="{{'options' | i18n}}">
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="copy(s)">
|
||||
<i class="fa fa-fw fa-copy" aria-hidden="true"></i>
|
||||
{{'copySendLink' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="removePassword(s)"
|
||||
*ngIf="s.password">
|
||||
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
|
||||
{{'removePassword' | i18n}}
|
||||
</a>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(s)">
|
||||
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
|
||||
{{'delete' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="no-items" *ngIf="filteredSends && !filteredSends.length">
|
||||
<ng-container *ngIf="!loaded">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="loaded">
|
||||
<p>{{'noSendsInList' | i18n}}</p>
|
||||
<button (click)="addSend()" class="btn btn-outline-primary">
|
||||
<i class="fa fa-plus fa-fw"></i>{{'createSend' | i18n}}</button>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #sendAddEdit></ng-template>
|
||||
221
src/app/send/send.component.ts
Normal file
221
src/app/send/send.component.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { SendType } from 'jslib/enums/sendType';
|
||||
|
||||
import { SendView } from 'jslib/models/view/sendView';
|
||||
|
||||
import { AddEditComponent } from './add-edit.component';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { EnvironmentService } from 'jslib/abstractions/environment.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { SendData } from 'jslib/models/data/sendData';
|
||||
|
||||
import { Send } from 'jslib/models/domain/send';
|
||||
|
||||
@Component({
|
||||
selector: 'app-send',
|
||||
templateUrl: 'send.component.html',
|
||||
})
|
||||
export class SendComponent implements OnInit {
|
||||
@ViewChild('sendAddEdit', { read: ViewContainerRef, static: true }) sendAddEditModalRef: ViewContainerRef;
|
||||
|
||||
sendType = SendType;
|
||||
loaded = false;
|
||||
loading = true;
|
||||
refreshing = false;
|
||||
expired: boolean = false;
|
||||
type: SendType = null;
|
||||
sends: SendView[] = [];
|
||||
filteredSends: SendView[] = [];
|
||||
searchText: string;
|
||||
selectedType: SendType;
|
||||
selectedAll: boolean;
|
||||
searchPlaceholder: string;
|
||||
filter: (cipher: SendView) => boolean;
|
||||
searchPending = false;
|
||||
|
||||
modal: ModalComponent = null;
|
||||
actionPromise: any;
|
||||
|
||||
private searchTimeout: any;
|
||||
|
||||
constructor(private apiService: ApiService, private userService: UserService,
|
||||
private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver,
|
||||
private platformUtilsService: PlatformUtilsService, private environmentService: EnvironmentService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
await this.load();
|
||||
}
|
||||
async load(filter: (send: SendView) => boolean = null) {
|
||||
this.loading = true;
|
||||
const userId = await this.userService.getUserId();
|
||||
const sends = await this.apiService.getSends();
|
||||
const sendsArr: SendView[] = [];
|
||||
if (sends != null && sends.data != null) {
|
||||
for (const res of sends.data) {
|
||||
const data = new SendData(res, userId);
|
||||
const send = new Send(data);
|
||||
const view = await send.decrypt();
|
||||
sendsArr.push(view);
|
||||
}
|
||||
}
|
||||
this.sends = sendsArr;
|
||||
this.selectAll();
|
||||
this.loading = false;
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async reload(filter: (send: SendView) => boolean = null) {
|
||||
this.loaded = false;
|
||||
this.sends = [];
|
||||
await this.load(filter);
|
||||
}
|
||||
|
||||
async refresh() {
|
||||
try {
|
||||
this.refreshing = true;
|
||||
await this.reload(this.filter);
|
||||
} finally {
|
||||
this.refreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async applyFilter(filter: (send: SendView) => boolean = null) {
|
||||
this.filter = filter;
|
||||
await this.search(null);
|
||||
}
|
||||
|
||||
async search(timeout: number = null) {
|
||||
this.searchPending = false;
|
||||
if (this.searchTimeout != null) {
|
||||
clearTimeout(this.searchTimeout);
|
||||
}
|
||||
if (timeout == null) {
|
||||
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
||||
return;
|
||||
}
|
||||
this.searchPending = true;
|
||||
this.searchTimeout = setTimeout(async () => {
|
||||
this.filteredSends = this.sends.filter((s) => this.filter == null || this.filter(s));
|
||||
this.searchPending = false;
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
addSend() {
|
||||
const component = this.editSend(null);
|
||||
component.type = this.type;
|
||||
}
|
||||
|
||||
editSend(send: SendView) {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.sendAddEditModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<AddEditComponent>(
|
||||
AddEditComponent, this.sendAddEditModalRef);
|
||||
|
||||
childComponent.sendId = send == null ? null : send.id;
|
||||
childComponent.onSavedSend.subscribe(async (s: SendView) => {
|
||||
this.modal.close();
|
||||
await this.load();
|
||||
});
|
||||
childComponent.onDeletedSend.subscribe(async (s: SendView) => {
|
||||
this.modal.close();
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
|
||||
return childComponent;
|
||||
}
|
||||
|
||||
async removePassword(s: SendView): Promise<boolean> {
|
||||
if (this.actionPromise != null || s.password == null) {
|
||||
return;
|
||||
}
|
||||
const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('removePasswordConfirmation'),
|
||||
this.i18nService.t('removePassword'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.actionPromise = this.apiService.putSendRemovePassword(s.id);
|
||||
await this.actionPromise;
|
||||
this.platformUtilsService.showToast('success', null, this.i18nService.t('removedPassword'));
|
||||
await this.load();
|
||||
} catch { }
|
||||
this.actionPromise = null;
|
||||
}
|
||||
|
||||
async delete(s: SendView): Promise<boolean> {
|
||||
if (this.actionPromise != null) {
|
||||
return false;
|
||||
}
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('deleteSendConfirmation'),
|
||||
this.i18nService.t('deleteSend'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.actionPromise = this.apiService.deleteSend(s.id);
|
||||
await this.actionPromise;
|
||||
this.platformUtilsService.showToast('success', null, this.i18nService.t('deletedSend'));
|
||||
await this.load();
|
||||
} catch { }
|
||||
this.actionPromise = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
copy(s: SendView) {
|
||||
let webVaultUrl = this.environmentService.getWebVaultUrl();
|
||||
if (webVaultUrl == null) {
|
||||
webVaultUrl = 'https://vault.bitwarden.com';
|
||||
}
|
||||
const link = webVaultUrl + '/#/send/' + s.accessId + '/' + s.urlB64Key;
|
||||
this.platformUtilsService.copyToClipboard(link);
|
||||
this.platformUtilsService.showToast('success', null,
|
||||
this.i18nService.t('valueCopied', this.i18nService.t('sendLink')));
|
||||
}
|
||||
|
||||
searchTextChanged() {
|
||||
this.search(200);
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
this.clearSelections();
|
||||
this.selectedAll = true;
|
||||
this.applyFilter(null);
|
||||
}
|
||||
|
||||
selectType(type: SendType) {
|
||||
this.clearSelections();
|
||||
this.selectedType = type;
|
||||
this.applyFilter((s) => s.type === type);
|
||||
}
|
||||
|
||||
clearSelections() {
|
||||
this.selectedAll = false;
|
||||
this.selectedType = null;
|
||||
}
|
||||
}
|
||||
@@ -73,8 +73,14 @@ export class EventService {
|
||||
msg = this.i18nService.t('editedItemId', this.formatCipherId(ev, options));
|
||||
break;
|
||||
case EventType.Cipher_Deleted:
|
||||
msg = this.i18nService.t('permanentlyDeletedItemId', this.formatCipherId(ev, options));
|
||||
break;
|
||||
case EventType.Cipher_SoftDeleted:
|
||||
msg = this.i18nService.t('deletedItemId', this.formatCipherId(ev, options));
|
||||
break;
|
||||
case EventType.Cipher_Restored:
|
||||
msg = this.i18nService.t('restoredItemId', this.formatCipherId(ev, options));
|
||||
break;
|
||||
case EventType.Cipher_AttachmentCreated:
|
||||
msg = this.i18nService.t('createdAttachmentForItem', this.formatCipherId(ev, options));
|
||||
break;
|
||||
|
||||
@@ -38,7 +38,6 @@ import { EventService as EventLoggingService } from 'jslib/services/event.servic
|
||||
import { ExportService } from 'jslib/services/export.service';
|
||||
import { FolderService } from 'jslib/services/folder.service';
|
||||
import { ImportService } from 'jslib/services/import.service';
|
||||
import { LockService } from 'jslib/services/lock.service';
|
||||
import { NotificationsService } from 'jslib/services/notifications.service';
|
||||
import { PasswordGenerationService } from 'jslib/services/passwordGeneration.service';
|
||||
import { PolicyService } from 'jslib/services/policy.service';
|
||||
@@ -49,6 +48,7 @@ import { SyncService } from 'jslib/services/sync.service';
|
||||
import { TokenService } from 'jslib/services/token.service';
|
||||
import { TotpService } from 'jslib/services/totp.service';
|
||||
import { UserService } from 'jslib/services/user.service';
|
||||
import { VaultTimeoutService } from 'jslib/services/vaultTimeout.service';
|
||||
import { WebCryptoFunctionService } from 'jslib/services/webCryptoFunction.service';
|
||||
|
||||
import { ApiService as ApiServiceAbstraction } from 'jslib/abstractions/api.service';
|
||||
@@ -65,7 +65,6 @@ import { ExportService as ExportServiceAbstraction } from 'jslib/abstractions/ex
|
||||
import { FolderService as FolderServiceAbstraction } from 'jslib/abstractions/folder.service';
|
||||
import { I18nService as I18nServiceAbstraction } from 'jslib/abstractions/i18n.service';
|
||||
import { ImportService as ImportServiceAbstraction } from 'jslib/abstractions/import.service';
|
||||
import { LockService as LockServiceAbstraction } from 'jslib/abstractions/lock.service';
|
||||
import { LogService as LogServiceAbstraction } from 'jslib/abstractions/log.service';
|
||||
import { MessagingService as MessagingServiceAbstraction } from 'jslib/abstractions/messaging.service';
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from 'jslib/abstractions/notifications.service';
|
||||
@@ -82,6 +81,7 @@ import { SyncService as SyncServiceAbstraction } from 'jslib/abstractions/sync.s
|
||||
import { TokenService as TokenServiceAbstraction } from 'jslib/abstractions/token.service';
|
||||
import { TotpService as TotpServiceAbstraction } from 'jslib/abstractions/totp.service';
|
||||
import { UserService as UserServiceAbstraction } from 'jslib/abstractions/user.service';
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from 'jslib/abstractions/vaultTimeout.service';
|
||||
|
||||
const i18nService = new I18nService(window.navigator.language, 'locales');
|
||||
const stateService = new StateService();
|
||||
@@ -106,10 +106,11 @@ const cipherService = new CipherService(cryptoService, userService, settingsServ
|
||||
const folderService = new FolderService(cryptoService, userService, apiService, storageService,
|
||||
i18nService, cipherService);
|
||||
const collectionService = new CollectionService(cryptoService, userService, storageService, i18nService);
|
||||
searchService = new SearchService(cipherService, platformUtilsService);
|
||||
searchService = new SearchService(cipherService);
|
||||
const policyService = new PolicyService(userService, storageService);
|
||||
const lockService = new LockService(cipherService, folderService, collectionService,
|
||||
cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, null);
|
||||
const vaultTimeoutService = new VaultTimeoutService(cipherService, folderService, collectionService,
|
||||
cryptoService, platformUtilsService, storageService, messagingService, searchService, userService, tokenService,
|
||||
null, async () => messagingService.send('logout', { expired: false }));
|
||||
const syncService = new SyncService(userService, apiService, settingsService,
|
||||
folderService, cipherService, cryptoService, collectionService, storageService, messagingService, policyService,
|
||||
async (expired: boolean) => messagingService.send('logout', { expired: expired }));
|
||||
@@ -117,11 +118,11 @@ const passwordGenerationService = new PasswordGenerationService(cryptoService, s
|
||||
const totpService = new TotpService(storageService, cryptoFunctionService);
|
||||
const containerService = new ContainerService(cryptoService);
|
||||
const authService = new AuthService(cryptoService, apiService,
|
||||
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService);
|
||||
userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService, vaultTimeoutService);
|
||||
const exportService = new ExportService(folderService, cipherService, apiService);
|
||||
const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService);
|
||||
const notificationsService = new NotificationsService(userService, syncService, appIdService,
|
||||
apiService, lockService, async () => messagingService.send('logout', { expired: true }));
|
||||
apiService, vaultTimeoutService, async () => messagingService.send('logout', { expired: true }));
|
||||
const environmentService = new EnvironmentService(apiService, storageService, notificationsService);
|
||||
const auditService = new AuditService(cryptoFunctionService, apiService);
|
||||
const eventLoggingService = new EventLoggingService(storageService, apiService, userService, cipherService);
|
||||
@@ -139,6 +140,8 @@ export function initFactory(): Function {
|
||||
} else {
|
||||
environmentService.notificationsUrl = isDev ? 'http://localhost:61840' :
|
||||
'https://notifications.bitwarden.com'; // window.location.origin + '/notifications';
|
||||
environmentService.enterpriseUrl = isDev ? 'http://localhost:52313' :
|
||||
'https://portal.bitwarden.com'; // window.location.origin + '/portal';
|
||||
}
|
||||
apiService.setUrls({
|
||||
base: isDev ? null : window.location.origin,
|
||||
@@ -156,7 +159,7 @@ export function initFactory(): Function {
|
||||
});
|
||||
setTimeout(() => notificationsService.init(environmentService), 3000);
|
||||
|
||||
lockService.init(true);
|
||||
vaultTimeoutService.init(true);
|
||||
const locale = await storageService.get<string>(ConstantsService.localeKey);
|
||||
await i18nService.init(locale);
|
||||
eventLoggingService.init(true);
|
||||
@@ -205,7 +208,7 @@ export function initFactory(): Function {
|
||||
{ provide: MessagingServiceAbstraction, useValue: messagingService },
|
||||
{ provide: BroadcasterService, useValue: broadcasterService },
|
||||
{ provide: SettingsServiceAbstraction, useValue: settingsService },
|
||||
{ provide: LockServiceAbstraction, useValue: lockService },
|
||||
{ provide: VaultTimeoutServiceAbstraction, useValue: vaultTimeoutService },
|
||||
{ provide: StorageServiceAbstraction, useValue: storageService },
|
||||
{ provide: StateServiceAbstraction, useValue: stateService },
|
||||
{ provide: ExportServiceAbstraction, useValue: exportService },
|
||||
|
||||
@@ -4,18 +4,18 @@ import {
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
|
||||
import { LockService } from 'jslib/abstractions/lock.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
|
||||
|
||||
@Injectable()
|
||||
export class UnauthGuardService implements CanActivate {
|
||||
constructor(private lockService: LockService, private userService: UserService,
|
||||
constructor(private vaultTimeoutService: VaultTimeoutService, private userService: UserService,
|
||||
private router: Router) { }
|
||||
|
||||
async canActivate() {
|
||||
const isAuthed = await this.userService.isAuthenticated();
|
||||
if (isAuthed) {
|
||||
const locked = await this.lockService.isLocked();
|
||||
const locked = await this.vaultTimeoutService.isLocked();
|
||||
if (locked) {
|
||||
this.router.navigate(['lock']);
|
||||
} else {
|
||||
|
||||
@@ -14,6 +14,14 @@
|
||||
<h1>{{'encKeySettings' | i18n}}</h1>
|
||||
</div>
|
||||
<app-change-kdf></app-change-kdf>
|
||||
<div class="secondary-header border-0 mb-0">
|
||||
<h1>{{'apiKey' | i18n}}</h1>
|
||||
</div>
|
||||
<p>
|
||||
{{'userApiKeyDesc' | i18n}}
|
||||
</p>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="viewUserApiKey()">{{'viewApiKey' | i18n}}</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="rotateUserApiKey()">{{'rotateApiKey' | i18n}}</button>
|
||||
<div class="secondary-header text-danger border-0 mb-0">
|
||||
<h1>{{'dangerZone' | i18n}}</h1>
|
||||
</div>
|
||||
@@ -30,3 +38,5 @@
|
||||
<ng-template #deauthorizeSessionsTemplate></ng-template>
|
||||
<ng-template #purgeVaultTemplate></ng-template>
|
||||
<ng-template #deleteAccountTemplate></ng-template>
|
||||
<ng-template #viewUserApiKeyTemplate></ng-template>
|
||||
<ng-template #rotateUserApiKeyTemplate></ng-template>
|
||||
|
||||
@@ -6,22 +6,29 @@ import {
|
||||
} from '@angular/core';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
import { ApiKeyComponent } from './api-key.component';
|
||||
import { DeauthorizeSessionsComponent } from './deauthorize-sessions.component';
|
||||
import { DeleteAccountComponent } from './delete-account.component';
|
||||
import { PurgeVaultComponent } from './purge-vault.component';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account',
|
||||
templateUrl: 'account.component.html',
|
||||
})
|
||||
export class AccountComponent {
|
||||
@ViewChild('deauthorizeSessionsTemplate', { read: ViewContainerRef }) deauthModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeVaultTemplate', { read: ViewContainerRef }) purgeModalRef: ViewContainerRef;
|
||||
@ViewChild('deleteAccountTemplate', { read: ViewContainerRef }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('deauthorizeSessionsTemplate', { read: ViewContainerRef, static: true }) deauthModalRef: ViewContainerRef;
|
||||
@ViewChild('purgeVaultTemplate', { read: ViewContainerRef, static: true }) purgeModalRef: ViewContainerRef;
|
||||
@ViewChild('deleteAccountTemplate', { read: ViewContainerRef, static: true }) deleteModalRef: ViewContainerRef;
|
||||
@ViewChild('viewUserApiKeyTemplate', { read: ViewContainerRef, static: true }) viewUserApiKeyModalRef: ViewContainerRef;
|
||||
@ViewChild('rotateUserApiKeyTemplate', { read: ViewContainerRef, static: true }) rotateUserApiKeyModalRef: ViewContainerRef;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
|
||||
constructor(private componentFactoryResolver: ComponentFactoryResolver, private apiService: ApiService,
|
||||
private userService: UserService) { }
|
||||
|
||||
deauthorizeSessions() {
|
||||
if (this.modal != null) {
|
||||
@@ -64,4 +71,49 @@ export class AccountComponent {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
async viewUserApiKey() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.viewUserApiKeyModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.viewUserApiKeyModalRef);
|
||||
childComponent.keyType = 'user';
|
||||
childComponent.entityId = await this.userService.getUserId();
|
||||
childComponent.postKey = this.apiService.postUserApiKey.bind(this.apiService);
|
||||
childComponent.scope = 'api';
|
||||
childComponent.grantType = 'client_credentials';
|
||||
childComponent.apiKeyTitle = 'apiKey';
|
||||
childComponent.apiKeyWarning = 'userApiKeyWarning';
|
||||
childComponent.apiKeyDescription = 'userApiKeyDesc';
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
async rotateUserApiKey() {
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.rotateUserApiKeyModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<ApiKeyComponent>(ApiKeyComponent, this.rotateUserApiKeyModalRef);
|
||||
childComponent.keyType = 'user';
|
||||
childComponent.isRotation = true;
|
||||
childComponent.entityId = await this.userService.getUserId();
|
||||
childComponent.postKey = this.apiService.postUserRotateApiKey.bind(this.apiService);
|
||||
childComponent.scope = 'api';
|
||||
childComponent.grantType = 'client_credentials';
|
||||
childComponent.apiKeyTitle = 'apiKey';
|
||||
childComponent.apiKeyWarning = 'userApiKeyWarning';
|
||||
childComponent.apiKeyDescription = 'apiKeyRotateDesc';
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export class AddCreditComponent implements OnInit {
|
||||
@Output() onAdded = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
@ViewChild('ppButtonForm', { read: ElementRef }) ppButtonFormRef: ElementRef;
|
||||
@ViewChild('ppButtonForm', { read: ElementRef, static: true }) ppButtonFormRef: ElementRef;
|
||||
|
||||
paymentMethodType = PaymentMethodType;
|
||||
ppButtonFormAction = WebConstants.paypal.buttonActionProduction;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
aria-hidden="true">×</span></button>
|
||||
<h3 class="card-body-header">{{(currentType != null ? 'changePaymentMethod' : 'addPaymentMethod') | i18n}}</h3>
|
||||
<app-payment [hideBank]="!organizationId" [hideCredit]="true"></app-payment>
|
||||
<app-tax-info (onCountryChanged)="changeCountry()"></app-tax-info>
|
||||
<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>
|
||||
|
||||
@@ -17,13 +17,15 @@ import { PaymentRequest } from 'jslib/models/request/paymentRequest';
|
||||
import { PaymentMethodType } from 'jslib/enums/paymentMethodType';
|
||||
|
||||
import { PaymentComponent } from './payment.component';
|
||||
import { TaxInfoComponent } from './tax-info.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-adjust-payment',
|
||||
templateUrl: 'adjust-payment.component.html',
|
||||
})
|
||||
export class AdjustPaymentComponent {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
@ViewChild(TaxInfoComponent, { static: true }) taxInfoComponent: TaxInfoComponent;
|
||||
|
||||
@Input() currentType?: PaymentMethodType;
|
||||
@Input() organizationId: string;
|
||||
@@ -42,9 +44,17 @@ export class AdjustPaymentComponent {
|
||||
this.formPromise = this.paymentComponent.createPaymentToken().then((result) => {
|
||||
request.paymentToken = result[0];
|
||||
request.paymentMethodType = result[1];
|
||||
request.postalCode = this.taxInfoComponent.taxInfo.postalCode;
|
||||
request.country = this.taxInfoComponent.taxInfo.country;
|
||||
if (this.organizationId == null) {
|
||||
return this.apiService.postAccountPayment(request);
|
||||
} else {
|
||||
request.taxId = this.taxInfoComponent.taxInfo.taxId;
|
||||
request.state = this.taxInfoComponent.taxInfo.state;
|
||||
request.line1 = this.taxInfoComponent.taxInfo.line1;
|
||||
request.line2 = this.taxInfoComponent.taxInfo.line2;
|
||||
request.city = this.taxInfoComponent.taxInfo.city;
|
||||
request.state = this.taxInfoComponent.taxInfo.state;
|
||||
return this.apiService.postOrganizationPayment(this.organizationId, request);
|
||||
}
|
||||
});
|
||||
@@ -60,4 +70,16 @@ export class AdjustPaymentComponent {
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
|
||||
changeCountry() {
|
||||
if (this.taxInfoComponent.taxInfo.country === 'US') {
|
||||
this.paymentComponent.hideBank = !this.organizationId;
|
||||
} else {
|
||||
this.paymentComponent.hideBank = true;
|
||||
if (this.paymentComponent.method === PaymentMethodType.BankAccount) {
|
||||
this.paymentComponent.method = PaymentMethodType.Card;
|
||||
this.paymentComponent.changeMethod();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export class AdjustStorageComponent {
|
||||
@Output() onAdjusted = new EventEmitter<number>();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(PaymentComponent, { static: true }) paymentComponent: PaymentComponent;
|
||||
|
||||
storageAdjustment = 0;
|
||||
formPromise: Promise<any>;
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="apiKeyTitle">{{'apiKey' | i18n}}</h2>
|
||||
<h2 class="modal-title" id="apiKeyTitle">{{apiKeyTitle | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" appA11yTitle="{{'close' | i18n}}">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{{'apiKeyDesc' | i18n}}</p>
|
||||
<p>{{apiKeyDescription | i18n}}</p>
|
||||
<ng-container *ngIf="!clientSecret">
|
||||
<label for="masterPassword">{{'masterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="MasterPasswordHash" class="form-control"
|
||||
[(ngModel)]="masterPassword" required appAutofocus appInputVerbatim>
|
||||
</ng-container>
|
||||
<app-callout type="warning" *ngIf="clientSecret">{{'apiKeyWarning' | i18n}}</app-callout>
|
||||
<app-callout type="warning" *ngIf="clientSecret">{{apiKeyWarning | i18n}}</app-callout>
|
||||
<app-callout type="info" title="{{'oauth2ClientCredentials' | i18n}}" icon="fa-key"
|
||||
*ngIf="clientSecret">
|
||||
<p class="mb-1">
|
||||
@@ -31,7 +31,7 @@
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<strong>grant_type:</strong><br>
|
||||
<code>client_credentials</code>
|
||||
<code>{{grantType}}</code>
|
||||
</p>
|
||||
</app-callout>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"
|
||||
*ngIf="!clientSecret">
|
||||
<i class="fa fa-spinner fa-spin" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span>{{'viewApiKey' | i18n}}</span>
|
||||
<span>{{(isRotation ? 'rotateApiKey' : 'viewApiKey') | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
@@ -17,17 +15,23 @@ import { ApiKeyResponse } from 'jslib/models/response/apiKeyResponse';
|
||||
templateUrl: 'api-key.component.html',
|
||||
})
|
||||
export class ApiKeyComponent {
|
||||
organizationId: string;
|
||||
keyType: string;
|
||||
isRotation: boolean;
|
||||
postKey: (entityId: string, request: PasswordVerificationRequest) => Promise<ApiKeyResponse>;
|
||||
entityId: string;
|
||||
scope: string;
|
||||
grantType: string;
|
||||
apiKeyTitle: string;
|
||||
apiKeyWarning: string;
|
||||
apiKeyDescription: string;
|
||||
|
||||
masterPassword: string;
|
||||
formPromise: Promise<ApiKeyResponse>;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
scope: string;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private router: Router) { }
|
||||
constructor(private i18nService: I18nService, private analytics: Angulartics2,
|
||||
private toasterService: ToasterService, private cryptoService: CryptoService) { }
|
||||
|
||||
async submit() {
|
||||
if (this.masterPassword == null || this.masterPassword === '') {
|
||||
@@ -39,12 +43,11 @@ export class ApiKeyComponent {
|
||||
const request = new PasswordVerificationRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.masterPassword, null);
|
||||
try {
|
||||
this.formPromise = this.apiService.postOrganizationApiKey(this.organizationId, request);
|
||||
this.formPromise = this.postKey(this.entityId, request);
|
||||
const response = await this.formPromise;
|
||||
this.clientSecret = response.apiKey;
|
||||
this.clientId = 'organization.' + this.organizationId;
|
||||
this.scope = 'api.organization';
|
||||
this.analytics.eventTrack.next({ action: 'Viewed Organization API Key' });
|
||||
this.clientId = `${this.keyType}.${this.entityId}`;
|
||||
this.analytics.eventTrack.next({ action: `Viewed ${this.keyType} API Key` });
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
@@ -28,18 +28,18 @@
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="newMasterPassword">{{'newMasterPass' | i18n}}</label>
|
||||
<input id="newMasterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
|
||||
[(ngModel)]="newMasterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
|
||||
<label for="masterPassword">{{'newMasterPass' | i18n}}</label>
|
||||
<input id="masterPassword" type="password" name="NewMasterPasswordHash" class="form-control mb-1"
|
||||
[(ngModel)]="masterPassword" (input)="updatePasswordStrength()" required appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
<app-password-strength [score]="masterPasswordScore" [showText]="true"></app-password-strength>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="confirmNewMasterPassword">{{'confirmNewMasterPass' | i18n}}</label>
|
||||
<input id="confirmNewMasterPassword" type="password" name="ConfirmNewMasterPasswordHash"
|
||||
class="form-control" [(ngModel)]="confirmNewMasterPassword" required appInputVerbatim
|
||||
<label for="masterPasswordRetype">{{'confirmNewMasterPass' | i18n}}</label>
|
||||
<input id="masterPasswordRetype" type="password" name="MasterPasswordRetype"
|
||||
class="form-control" [(ngModel)]="masterPasswordRetype" required appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CipherService } from 'jslib/abstractions/cipher.service';
|
||||
@@ -18,8 +12,11 @@ import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import {
|
||||
ChangePasswordComponent as BaseChangePasswordComponent,
|
||||
} from 'jslib/angular/components/change-password.component';
|
||||
|
||||
import { CipherString } from 'jslib/models/domain/cipherString';
|
||||
import { MasterPasswordPolicyOptions } from 'jslib/models/domain/masterPasswordPolicyOptions';
|
||||
import { SymmetricCryptoKey } from 'jslib/models/domain/symmetricCryptoKey';
|
||||
|
||||
import { CipherWithIdRequest } from 'jslib/models/request/cipherWithIdRequest';
|
||||
@@ -31,136 +28,18 @@ import { UpdateKeyRequest } from 'jslib/models/request/updateKeyRequest';
|
||||
selector: 'app-change-password',
|
||||
templateUrl: 'change-password.component.html',
|
||||
})
|
||||
export class ChangePasswordComponent implements OnInit {
|
||||
currentMasterPassword: string;
|
||||
newMasterPassword: string;
|
||||
confirmNewMasterPassword: string;
|
||||
formPromise: Promise<any>;
|
||||
masterPasswordScore: number;
|
||||
export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||
rotateEncKey = false;
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
currentMasterPassword: string;
|
||||
|
||||
private masterPasswordStrengthTimeout: any;
|
||||
private email: string;
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
private cryptoService: CryptoService, private messagingService: MessagingService,
|
||||
private userService: UserService, private passwordGenerationService: PasswordGenerationService,
|
||||
private platformUtilsService: PlatformUtilsService, private folderService: FolderService,
|
||||
private cipherService: CipherService, private syncService: SyncService,
|
||||
private policyService: PolicyService) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.email = await this.userService.getEmail();
|
||||
this.enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
|
||||
}
|
||||
|
||||
getPasswordScoreAlertDisplay() {
|
||||
if (this.enforcedPolicyOptions == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let str: string;
|
||||
switch (this.enforcedPolicyOptions.minComplexity) {
|
||||
case 4:
|
||||
str = this.i18nService.t('strong');
|
||||
break;
|
||||
case 3:
|
||||
str = this.i18nService.t('good');
|
||||
break;
|
||||
default:
|
||||
str = this.i18nService.t('weak');
|
||||
break;
|
||||
}
|
||||
return str + ' (' + this.enforcedPolicyOptions.minComplexity + ')';
|
||||
}
|
||||
|
||||
async submit() {
|
||||
const hasEncKey = await this.cryptoService.hasEncKey();
|
||||
if (!hasEncKey) {
|
||||
this.toasterService.popAsync('error', null, this.i18nService.t('updateKey'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentMasterPassword == null || this.currentMasterPassword === '' ||
|
||||
this.newMasterPassword == null || this.newMasterPassword === '') {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassRequired'));
|
||||
return;
|
||||
}
|
||||
if (this.newMasterPassword.length < 8) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassLength'));
|
||||
return;
|
||||
}
|
||||
if (this.newMasterPassword !== this.confirmNewMasterPassword) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassDoesntMatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
|
||||
this.getPasswordStrengthUserInput());
|
||||
|
||||
if (this.enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
strengthResult.score,
|
||||
this.newMasterPassword,
|
||||
this.enforcedPolicyOptions)) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPasswordPolicyRequirementsNotMet'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (strengthResult != null && strengthResult.score < 3) {
|
||||
const result = await this.platformUtilsService.showDialog(this.i18nService.t('weakMasterPasswordDesc'),
|
||||
this.i18nService.t('weakMasterPassword'), this.i18nService.t('yes'), this.i18nService.t('no'),
|
||||
'warning');
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.rotateEncKey) {
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null);
|
||||
const email = await this.userService.getEmail();
|
||||
const kdf = await this.userService.getKdf();
|
||||
const kdfIterations = await this.userService.getKdfIterations();
|
||||
const newKey = await this.cryptoService.makeKey(this.newMasterPassword, email.trim().toLowerCase(),
|
||||
kdf, kdfIterations);
|
||||
request.newMasterPasswordHash = await this.cryptoService.hashPassword(this.newMasterPassword, newKey);
|
||||
const newEncKey = await this.cryptoService.remakeEncKey(newKey);
|
||||
request.key = newEncKey[1].encryptedString;
|
||||
try {
|
||||
if (this.rotateEncKey) {
|
||||
this.formPromise = this.apiService.postPassword(request).then(() => {
|
||||
return this.updateKey(newKey, request.newMasterPasswordHash);
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.apiService.postPassword(request);
|
||||
}
|
||||
await this.formPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Changed Password' });
|
||||
this.toasterService.popAsync('success', this.i18nService.t('masterPasswordChanged'),
|
||||
this.i18nService.t('logBackIn'));
|
||||
this.messagingService.send('logout');
|
||||
} catch { }
|
||||
}
|
||||
|
||||
updatePasswordStrength() {
|
||||
if (this.masterPasswordStrengthTimeout != null) {
|
||||
clearTimeout(this.masterPasswordStrengthTimeout);
|
||||
}
|
||||
this.masterPasswordStrengthTimeout = setTimeout(() => {
|
||||
const strengthResult = this.passwordGenerationService.passwordStrength(this.newMasterPassword,
|
||||
this.getPasswordStrengthUserInput());
|
||||
this.masterPasswordScore = strengthResult == null ? null : strengthResult.score;
|
||||
}, 300);
|
||||
constructor(i18nService: I18nService,
|
||||
cryptoService: CryptoService, messagingService: MessagingService,
|
||||
userService: UserService, passwordGenerationService: PasswordGenerationService,
|
||||
platformUtilsService: PlatformUtilsService, policyService: PolicyService,
|
||||
private folderService: FolderService, private cipherService: CipherService,
|
||||
private syncService: SyncService, private apiService: ApiService, ) {
|
||||
super(i18nService, cryptoService, messagingService, userService, passwordGenerationService,
|
||||
platformUtilsService, policyService);
|
||||
}
|
||||
|
||||
async rotateEncKeyClicked() {
|
||||
@@ -198,13 +77,54 @@ export class ChangePasswordComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private getPasswordStrengthUserInput() {
|
||||
let userInput: string[] = [];
|
||||
const atPosition = this.email.indexOf('@');
|
||||
if (atPosition > -1) {
|
||||
userInput = userInput.concat(this.email.substr(0, atPosition).trim().toLowerCase().split(/[^A-Za-z0-9]/));
|
||||
async submit() {
|
||||
const hasEncKey = await this.cryptoService.hasEncKey();
|
||||
if (!hasEncKey) {
|
||||
this.platformUtilsService.showToast('error', null, this.i18nService.t('updateKey'));
|
||||
return;
|
||||
}
|
||||
|
||||
await super.submit();
|
||||
}
|
||||
|
||||
async setupSubmitActions() {
|
||||
if (this.currentMasterPassword == null || this.currentMasterPassword === '') {
|
||||
this.platformUtilsService.showToast('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('masterPassRequired'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.rotateEncKey) {
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
|
||||
return super.setupSubmitActions();
|
||||
}
|
||||
|
||||
async performSubmitActions(newMasterPasswordHash: string, newKey: SymmetricCryptoKey,
|
||||
newEncKey: [SymmetricCryptoKey, CipherString]) {
|
||||
const request = new PasswordRequest();
|
||||
request.masterPasswordHash = await this.cryptoService.hashPassword(this.currentMasterPassword, null);
|
||||
request.newMasterPasswordHash = newMasterPasswordHash;
|
||||
request.key = newEncKey[1].encryptedString;
|
||||
|
||||
try {
|
||||
if (this.rotateEncKey) {
|
||||
this.formPromise = this.apiService.postPassword(request).then(() => {
|
||||
return this.updateKey(newKey, request.newMasterPasswordHash);
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.apiService.postPassword(request);
|
||||
}
|
||||
|
||||
await this.formPromise;
|
||||
|
||||
this.platformUtilsService.showToast('success', this.i18nService.t('masterPasswordChanged'),
|
||||
this.i18nService.t('logBackIn'));
|
||||
this.messagingService.send('logout');
|
||||
} catch {
|
||||
this.platformUtilsService.showToast('error', null, this.i18nService.t('errorOccurred'));
|
||||
}
|
||||
return userInput;
|
||||
}
|
||||
|
||||
private async updateKey(key: SymmetricCryptoKey, masterPasswordHash: string) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { OrganizationPlansComponent } from './organization-plans.component';
|
||||
templateUrl: 'create-organization.component.html',
|
||||
})
|
||||
export class CreateOrganizationComponent implements OnInit {
|
||||
@ViewChild(OrganizationPlansComponent) orgPlansComponent: OrganizationPlansComponent;
|
||||
@ViewChild(OrganizationPlansComponent, { static: true }) orgPlansComponent: OrganizationPlansComponent;
|
||||
|
||||
constructor(private route: ActivatedRoute) { }
|
||||
|
||||
|
||||
4
src/app/settings/link-sso.component.html
Normal file
4
src/app/settings/link-sso.component.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<a class="dropdown-item" href="#" appStopClick (click)="submit(returnUri, true)">
|
||||
<i class="fa fa-fw fa-link" aria-hidden="true"></i>
|
||||
{{'linkSso' | i18n}}
|
||||
</a>
|
||||
48
src/app/settings/link-sso.component.ts
Normal file
48
src/app/settings/link-sso.component.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
AfterContentInit,
|
||||
Component,
|
||||
Input,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { AuthService } from 'jslib/abstractions/auth.service';
|
||||
import { CryptoFunctionService } from 'jslib/abstractions/cryptoFunction.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
|
||||
import { SsoComponent } from 'jslib/angular/components/sso.component';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
@Component({
|
||||
selector: 'app-link-sso',
|
||||
templateUrl: 'link-sso.component.html',
|
||||
})
|
||||
export class LinkSsoComponent extends SsoComponent implements AfterContentInit {
|
||||
@Input() organization: Organization;
|
||||
returnUri: string = '/settings/organizations'
|
||||
|
||||
constructor(platformUtilsService: PlatformUtilsService, i18nService: I18nService,
|
||||
apiService: ApiService, authService: AuthService,
|
||||
router: Router, route: ActivatedRoute,
|
||||
cryptoFunctionService: CryptoFunctionService, passwordGenerationService: PasswordGenerationService,
|
||||
storageService: StorageService, stateService: StateService) {
|
||||
super(authService, router,
|
||||
i18nService, route,
|
||||
storageService, stateService,
|
||||
platformUtilsService, apiService,
|
||||
cryptoFunctionService, passwordGenerationService);
|
||||
|
||||
this.returnUri = '/settings/organizations';
|
||||
this.redirectUri = window.location.origin + '/sso-connector.html';
|
||||
this.clientId = 'web';
|
||||
}
|
||||
|
||||
async ngAfterContentInit() {
|
||||
this.identifier = this.organization.identifier;
|
||||
}
|
||||
}
|
||||
@@ -6,14 +6,33 @@
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="lockOption">{{'lockOptions' | i18n}}</label>
|
||||
<select id="lockOption" name="LockOption" [(ngModel)]="lockOption" class="form-control">
|
||||
<option *ngFor="let o of lockOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
<label for="vaultTimeout">{{'vaultTimeout' | i18n}}</label>
|
||||
<select id="vaultTimeout" name="VaultTimeout" [(ngModel)]="vaultTimeout" class="form-control">
|
||||
<option *ngFor="let o of vaultTimeouts" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">{{'lockOptionsDesc' | i18n}}</small>
|
||||
<small class="form-text text-muted">{{'vaultTimeoutDesc' | i18n}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{'vaultTimeoutAction' | i18n}}</label>
|
||||
<div class="form-check form-check-block">
|
||||
<input class="form-check-input" type="radio" name="vaultTimeoutAction" id="vaultTimeoutActionLock"
|
||||
value="lock" [(ngModel)]="vaultTimeoutAction">
|
||||
<label class="form-check-label" for="vaultTimeoutActionLock">
|
||||
{{'lock' | i18n}}
|
||||
<small>{{'vaultTimeoutActionLockDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mt-2 form-check-block">
|
||||
<input class="form-check-input" type="radio" name="vaultTimeoutAction" id="vaultTimeoutActionLogOut"
|
||||
value="logOut" [(ngModel)]="vaultTimeoutAction" (ngModelChange)="vaultTimeoutActionChanged($event)">
|
||||
<label class="form-check-label" for="vaultTimeoutActionLogOut">
|
||||
{{'logOut' | i18n}}
|
||||
<small>{{'vaultTimeoutActionLogOutDesc' | i18n}}</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
@@ -57,6 +76,16 @@
|
||||
</a>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{'enableGravatarsDesc' | i18n}}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="enableFullWidth" name="enableFullWidth"
|
||||
[(ngModel)]="enableFullWidth">
|
||||
<label class="form-check-label" for="enableFullWidth">
|
||||
{{'enableFullWidth' | i18n}}
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">{{'enableFullWidthDesc' | i18n}}</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{'save' | i18n}}
|
||||
|
||||
@@ -7,10 +7,11 @@ import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { LockService } from 'jslib/abstractions/lock.service';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { StateService } from 'jslib/abstractions/state.service';
|
||||
import { StorageService } from 'jslib/abstractions/storage.service';
|
||||
import { VaultTimeoutService } from 'jslib/abstractions/vaultTimeout.service';
|
||||
|
||||
import { ConstantsService } from 'jslib/services/constants.service';
|
||||
|
||||
@@ -21,20 +22,22 @@ import { Utils } from 'jslib/misc/utils';
|
||||
templateUrl: 'options.component.html',
|
||||
})
|
||||
export class OptionsComponent implements OnInit {
|
||||
lockOption: number = null;
|
||||
vaultTimeout: number = null;
|
||||
vaultTimeoutAction: string = 'lock';
|
||||
disableIcons: boolean;
|
||||
enableGravatars: boolean;
|
||||
enableFullWidth: boolean;
|
||||
locale: string;
|
||||
lockOptions: any[];
|
||||
vaultTimeouts: any[];
|
||||
localeOptions: any[];
|
||||
|
||||
private startingLocale: string;
|
||||
|
||||
constructor(private storageService: StorageService, private stateService: StateService,
|
||||
private analytics: Angulartics2, private i18nService: I18nService,
|
||||
private toasterService: ToasterService, private lockService: LockService,
|
||||
private platformUtilsService: PlatformUtilsService) {
|
||||
this.lockOptions = [
|
||||
private toasterService: ToasterService, private vaultTimeoutService: VaultTimeoutService,
|
||||
private platformUtilsService: PlatformUtilsService, private messagingService: MessagingService) {
|
||||
this.vaultTimeouts = [
|
||||
{ name: i18nService.t('oneMinute'), value: 1 },
|
||||
{ name: i18nService.t('fiveMinutes'), value: 5 },
|
||||
{ name: i18nService.t('fifteenMinutes'), value: 15 },
|
||||
@@ -44,7 +47,7 @@ export class OptionsComponent implements OnInit {
|
||||
{ name: i18nService.t('onRefresh'), value: -1 },
|
||||
];
|
||||
if (this.platformUtilsService.isDev()) {
|
||||
this.lockOptions.push({ name: i18nService.t('never'), value: null });
|
||||
this.vaultTimeouts.push({ name: i18nService.t('never'), value: null });
|
||||
}
|
||||
|
||||
const localeOptions: any[] = [];
|
||||
@@ -61,18 +64,23 @@ export class OptionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.lockOption = await this.storageService.get<number>(ConstantsService.lockOptionKey);
|
||||
this.vaultTimeout = await this.storageService.get<number>(ConstantsService.vaultTimeoutKey);
|
||||
this.vaultTimeoutAction = await this.storageService.get<string>(ConstantsService.vaultTimeoutActionKey);
|
||||
this.disableIcons = await this.storageService.get<boolean>(ConstantsService.disableFaviconKey);
|
||||
this.enableGravatars = await this.storageService.get<boolean>('enableGravatars');
|
||||
this.enableFullWidth = await this.storageService.get<boolean>('enableFullWidth');
|
||||
this.locale = this.startingLocale = await this.storageService.get<string>(ConstantsService.localeKey);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
await this.lockService.setLockOption(this.lockOption != null ? this.lockOption : null);
|
||||
await this.vaultTimeoutService.setVaultTimeoutOptions(this.vaultTimeout != null ? this.vaultTimeout : null,
|
||||
this.vaultTimeoutAction);
|
||||
await this.storageService.save(ConstantsService.disableFaviconKey, this.disableIcons);
|
||||
await this.stateService.save(ConstantsService.disableFaviconKey, this.disableIcons);
|
||||
await this.storageService.save('enableGravatars', this.enableGravatars);
|
||||
await this.stateService.save('enableGravatars', this.enableGravatars);
|
||||
await this.storageService.save('enableFullWidth', this.enableFullWidth);
|
||||
this.messagingService.send('setFullWidth');
|
||||
await this.storageService.save(ConstantsService.localeKey, this.locale);
|
||||
this.analytics.eventTrack.next({ action: 'Saved Options' });
|
||||
if (this.locale !== this.startingLocale) {
|
||||
@@ -81,4 +89,18 @@ export class OptionsComponent implements OnInit {
|
||||
this.toasterService.popAsync('success', null, this.i18nService.t('optionsUpdated'));
|
||||
}
|
||||
}
|
||||
|
||||
async vaultTimeoutActionChanged(newValue: string) {
|
||||
if (newValue === 'logOut') {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('vaultTimeoutLogOutConfirmation'),
|
||||
this.i18nService.t('vaultTimeoutLogOutConfirmationTitle'),
|
||||
this.i18nService.t('yes'), this.i18nService.t('cancel'), 'warning');
|
||||
if (!confirmed) {
|
||||
this.vaultTimeoutAction = 'lock';
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.vaultTimeoutAction = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<ng-container *ngIf="loading">
|
||||
<i class="fa fa-spinner fa-spin text-muted" title="{{'loading' | i18n}}" aria-hidden="true"></i>
|
||||
<span class="sr-only">{{'loading' | i18n}}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="createOrganization && selfHosted">
|
||||
<p>{{'uploadLicenseFileOrg' | i18n}}</p>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
@@ -13,7 +17,8 @@
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate
|
||||
*ngIf="!loading && !selfHosted && this.plans">
|
||||
<h2 class="mt-5">{{'generalInformation' | i18n}}</h2>
|
||||
<div class="row" *ngIf="createOrganization">
|
||||
<div class="form-group col-6">
|
||||
@@ -38,69 +43,61 @@
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="mt-5">{{'chooseYourPlan' | i18n}}</h2>
|
||||
<div class="form-check form-check-block" *ngIf="!ownedBusiness && showFree">
|
||||
<input class="form-check-input" type="radio" name="PlanType" id="planFree" value="free" [(ngModel)]="plan"
|
||||
(change)="changedPlan()">
|
||||
<label class="form-check-label" for="planFree">
|
||||
{{'planNameFree' | i18n}}
|
||||
<small class="mb-1">{{'planDescFree' | i18n : '1'}}</small>
|
||||
<small>• {{'limitedUsers' | i18n : '2'}}</small>
|
||||
<small>• {{'limitedCollections' | i18n : '2'}}</small>
|
||||
<span>{{'freeForever' | i18n}}</span>
|
||||
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block">
|
||||
<input class="form-check-input" type="radio" name="product" id="product{{selectableProduct.product}}"
|
||||
[value]="selectableProduct.product" [(ngModel)]="product" (change)="changedProduct()">
|
||||
<label class="form-check-label" for="product{{selectableProduct.product}}">
|
||||
{{ selectableProduct.nameLocalizationKey | i18n}}
|
||||
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n : '1'}}</small>
|
||||
<ng-container *ngIf="selectableProduct.product === productTypes.Enterprise; else fullFeatureList">
|
||||
<small>• {{'includeAllTeamsFeatures' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasSelfHost">• {{'onPremHostingOptional' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasSso">• {{'includeSsoAuthentication' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasPolicies">• {{'includeEnterprisePolicies' | i18n}}</small>
|
||||
</ng-container>
|
||||
<ng-template #fullFeatureList>
|
||||
<small *ngIf="selectableProduct.product == productTypes.Free">•
|
||||
{{'limitedUsers' | i18n : selectableProduct.maxUsers }}</small>
|
||||
<small *ngIf="selectableProduct.product != productTypes.Free && selectableProduct.maxUsers">•
|
||||
{{'addShareLimitedUsers' | i18n : selectableProduct.maxUsers}}</small>
|
||||
<small *ngIf="!selectableProduct.maxUsers">•
|
||||
{{'addShareUnlimitedUsers' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.maxCollections">•
|
||||
{{'limitedCollections' | i18n : selectableProduct.maxCollections }}</small>
|
||||
<small *ngIf="selectableProduct.maxAdditionalSeats">•
|
||||
{{'addShareLimitedUsers' | i18n : selectableProduct.maxAdditionalSeats }}</small>
|
||||
<small *ngIf="!selectableProduct.maxCollections">• {{'createUnlimitedCollections' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.baseStorageGb">•
|
||||
{{'gbEncryptedFileStorage' | i18n : selectableProduct.baseStorageGb + 'GB'}}</small>
|
||||
<small *ngIf="selectableProduct.hasGroups">• {{'controlAccessWithGroups' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasApi">• {{'trackAuditLogs' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasDirectory">• {{'syncUsersFromDirectory' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.hasSelfHost">• {{'onPremHostingOptional' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.usersGetPremium">• {{'usersGetPremium' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.product != productTypes.Free">•
|
||||
{{'priorityCustomerSupport' | i18n}}</small>
|
||||
<small *ngIf="selectableProduct.trialPeriodDays">•
|
||||
{{'xDayFreeTrial' | i18n : selectableProduct.trialPeriodDays }}
|
||||
</small>
|
||||
</ng-template>
|
||||
<span *ngIf="selectableProduct.product != productTypes.Free">
|
||||
<ng-container *ngIf="selectableProduct.basePrice">
|
||||
{{selectableProduct.basePrice / 12 | currency:'$'}} /{{'month' | i18n}},
|
||||
{{'includesXUsers' | i18n : selectableProduct.baseSeats}}
|
||||
<ng-container *ngIf="selectableProduct.hasAdditionalSeatsOption">
|
||||
{{('additionalUsers' | i18n).toLowerCase()}}
|
||||
{{selectableProduct.seatPrice / 12 | currency:'$'}} /{{'month' | i18n}}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</span>
|
||||
<span *ngIf="!selectableProduct.basePrice && selectableProduct.hasAdditionalSeatsOption">
|
||||
{{'costPerUser' | i18n : (selectableProduct.seatPrice / 12 | currency:'$')}} /{{'month' | i18n}}
|
||||
</span>
|
||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{'freeForever' | i18n}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-block" *ngIf="!ownedBusiness">
|
||||
<input class="form-check-input" type="radio" name="PlanType" id="planFamilies" value="families"
|
||||
[(ngModel)]="plan" (change)="changedPlan()">
|
||||
<label class="form-check-label" for="planFamilies">
|
||||
{{'planNameFamilies' | i18n}}
|
||||
<small class="mb-1">{{'planDescFamilies' | i18n}}</small>
|
||||
<small>• {{'addShareLimitedUsers' | i18n : '5'}}</small>
|
||||
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
||||
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
||||
<small>• {{'onPremHostingOptional' | i18n}}</small>
|
||||
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
||||
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
||||
<span>{{1 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-block">
|
||||
<input class="form-check-input" type="radio" name="PlanType" id="planTeams" value="teams" [(ngModel)]="plan"
|
||||
(change)="changedPlan()">
|
||||
<label class="form-check-label" for="planTeams">
|
||||
{{'planNameTeams' | i18n}}
|
||||
<small class="mb-1">{{'planDescTeams' | i18n}}</small>
|
||||
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
|
||||
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
||||
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
||||
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
||||
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
||||
<span>{{5 | currency:'$'}} /{{'month' | i18n}}, {{'includesXUsers' | i18n : 5}},
|
||||
{{('additionalUsers' | i18n).toLowerCase()}}
|
||||
{{2 | currency:'$'}} /{{'month' | i18n}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-block">
|
||||
<input class="form-check-input" type="radio" name="PlanType" id="planEnterprise" value="enterprise"
|
||||
[(ngModel)]="plan" (change)="changedPlan()">
|
||||
<label class="form-check-label" for="planEnterprise">
|
||||
{{'planNameEnterprise' | i18n}}
|
||||
<small class="mb-1">{{'planDescEnterprise' | i18n}}</small>
|
||||
<small>• {{'addShareUnlimitedUsers' | i18n}}</small>
|
||||
<small>• {{'createUnlimitedCollections' | i18n}}</small>
|
||||
<small>• {{'gbEncryptedFileStorage' | i18n : '1 GB'}}</small>
|
||||
<small>• {{'controlAccessWithGroups' | i18n}}</small>
|
||||
<small>• {{'trackAuditLogs' | i18n}}</small>
|
||||
<small>• {{'syncUsersFromDirectory' | i18n}}</small>
|
||||
<small>• {{'onPremHostingOptional' | i18n}}</small>
|
||||
<small>• {{'usersGetPremium' | i18n}}</small>
|
||||
<small>• {{'priorityCustomerSupport' | i18n}}</small>
|
||||
<small>• {{'xDayFreeTrial' | i18n : '7'}}</small>
|
||||
<span>{{'costPerUser' | i18n : (3 | currency:'$')}} /{{'month' | i18n}}</span>
|
||||
</label>
|
||||
</div>
|
||||
<ng-container *ngIf="!plans[plan].noPayment">
|
||||
<ng-container *ngIf="!plans[plan].noAdditionalSeats && !plans[plan].baseSeats">
|
||||
<div *ngIf="product !== productTypes.Free">
|
||||
<ng-container *ngIf="selectedPlan.hasAdditionalSeatsOption && !selectedPlan.baseSeats">
|
||||
<h2 class="mt-5">{{'users' | i18n}}</h2>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
@@ -113,13 +110,13 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="mt-5">{{'addons' | i18n}}</h2>
|
||||
<div class="row" *ngIf="!plans[plan].noAdditionalSeats && plans[plan].baseSeats">
|
||||
<div class="row" *ngIf="selectedPlan.hasAdditionalSeatsOption && selectedPlan.baseSeats">
|
||||
<div class="form-group col-6">
|
||||
<label for="additionalSeats">{{'additionalUserSeats' | i18n}}</label>
|
||||
<input id="additionalSeats" class="form-control" type="number" name="AdditionalSeats"
|
||||
[(ngModel)]="additionalSeats" min="0" max="100000" placeholder="{{'userSeatsDesc' | i18n}}">
|
||||
<small
|
||||
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : plans[plan].baseSeats : (plans[plan].seatPrice | currency:'$')}}</small>
|
||||
class="text-muted form-text">{{'userSeatsAdditionalDesc' | i18n : selectedPlan.baseSeats : (seatPriceMonthly(selectedPlan) | currency:'$')}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -129,11 +126,11 @@
|
||||
[(ngModel)]="additionalStorage" min="0" max="99" step="1"
|
||||
placeholder="{{'additionalStorageGbDesc' | i18n}}">
|
||||
<small
|
||||
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (storageGb.price | currency:'$') : ('month' | i18n)}}</small>
|
||||
class="text-muted form-text">{{'additionalStorageIntervalDesc' | i18n : '1 GB' : (additionalStoragePriceMonthly(selectedPlan) | currency:'$') : ('month' | i18n)}}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-6" *ngIf="plans[plan].canBuyPremiumAccessAddon">
|
||||
<div class="form-group col-6" *ngIf="selectedPlan.hasPremiumAccessOption">
|
||||
<div class="form-check">
|
||||
<input id="premiumAccess" class="form-check-input" type="checkbox" name="PremiumAccessAddon"
|
||||
[(ngModel)]="premiumAccessAddon">
|
||||
@@ -144,72 +141,91 @@
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{'summary' | i18n}}</h2>
|
||||
<div class="form-check form-check-block">
|
||||
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalAnnually" value="year"
|
||||
[(ngModel)]="interval">
|
||||
<label class="form-check-label" for="intervalAnnually">
|
||||
{{'annually' | i18n}}
|
||||
<small *ngIf="plans[plan].annualBasePrice">
|
||||
{{'basePrice' | i18n}}: {{plans[plan].basePrice | currency:'$'}} ×12 {{'monthAbbr' | i18n}} =
|
||||
{{baseTotal(true) | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="!plans[plan].noAdditionalSeats">
|
||||
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
|
||||
{{additionalSeats || 0}} × {{plans[plan].seatPrice | currency:'$'}} ×12
|
||||
{{'monthAbbr' | i18n}} = {{seatTotal(true)
|
||||
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans">
|
||||
<input class="form-check-input" type="radio" name="BillingInterval" id="interval{{selectablePlan.type}}"
|
||||
[value]="selectablePlan.type" [(ngModel)]="plan">
|
||||
<label class="form-check-label" for="interval{{selectablePlan.type}}">
|
||||
<ng-container *ngIf="selectablePlan.isAnnual">
|
||||
{{'annually' | i18n}}
|
||||
<small *ngIf="selectablePlan.basePrice">
|
||||
{{'basePrice' | i18n}}: {{ selectablePlan.basePrice / 12 | currency:'$'}} × 12
|
||||
{{'monthAbbr' | i18n}}
|
||||
=
|
||||
{{selectablePlan.basePrice | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
|
||||
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
|
||||
{{additionalSeats || 0}} × {{selectablePlan.seatPrice / 12 | currency:'$'}} × 12
|
||||
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
|
||||
| currency:'$'}} /{{'year' | i18n}}
|
||||
</small>
|
||||
<small>
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{storageGb.price | currency:'$'}} ×12 {{'monthAbbr'
|
||||
| i18n}} = {{additionalStorageTotal(true) | currency:'$'}} /{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="plans[plan].canBuyPremiumAccessAddon && premiumAccessAddon">
|
||||
{{'premiumAccess' | i18n}}:
|
||||
{{3.33 | currency:'$'}} ×12 {{'monthAbbr' | i18n}} = {{40 | currency:'$'}} /{{'year' | i18n}}
|
||||
</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check form-check-block" *ngIf="plans[plan].monthlySeatPrice">
|
||||
<input class="form-check-input" type="radio" name="BillingInterval" id="intervalMonthly" value="month"
|
||||
[(ngModel)]="interval">
|
||||
<label class="form-check-label" for="intervalMonthly">
|
||||
{{'monthly' | i18n}}
|
||||
<small *ngIf="plans[plan].monthlyBasePrice">
|
||||
{{'basePrice' | i18n}}: {{baseTotal(false) | currency:'$'}} /{{'month' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="!plans[plan].noAdditionalSeats">
|
||||
<span *ngIf="plans[plan].baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||
<span *ngIf="!plans[plan].baseSeats">{{'users' | i18n}}:</span>
|
||||
{{additionalSeats || 0}} × {{plans[plan].monthlySeatPrice | currency:'$'}} =
|
||||
{{seatTotal(false) | currency:'$'}} /{{'month'
|
||||
| i18n}}
|
||||
</small>
|
||||
<small>
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{storageGb.monthlyPrice | currency:'$'}} = {{additionalStorageTotal(false)
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{selectablePlan.additionalStoragePricePerGb / 12 | currency:'$'}} × 12 {{'monthAbbr'
|
||||
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
|
||||
{{'premiumAccess' | i18n}}:
|
||||
{{selectablePlan.premiumAccessOptionCost / 12 | currency:'$'}} × 12 {{'monthAbbr' | i18n}}
|
||||
=
|
||||
{{40 | currency:'$'}}
|
||||
/{{'year' | i18n}}
|
||||
</small>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!selectablePlan.isAnnual">
|
||||
{{'monthly' | i18n}}
|
||||
<small *ngIf="selectablePlan.basePrice">
|
||||
{{'basePrice' | i18n}}: {{selectablePlan.basePrice | currency:'$'}} {{'monthAbbr' | i18n}}
|
||||
=
|
||||
{{selectablePlan.basePrice | currency:'$'}}
|
||||
/{{'month' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalSeatsOption">
|
||||
<span *ngIf="selectablePlan.baseSeats">{{'additionalUsers' | i18n}}:</span>
|
||||
<span *ngIf="!selectablePlan.baseSeats">{{'users' | i18n}}:</span>
|
||||
{{additionalSeats || 0}} × {{selectablePlan.seatPrice | currency:'$'}}
|
||||
{{'monthAbbr' | i18n}} = {{seatTotal(selectablePlan)
|
||||
| currency:'$'}} /{{'month' | i18n}}
|
||||
</small>
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasAdditionalStorageOption">
|
||||
{{'additionalStorageGb' | i18n}}: {{additionalStorage || 0}} ×
|
||||
{{selectablePlan.additionalStoragePricePerGb | currency:'$'}} {{'monthAbbr'
|
||||
| i18n}} = {{additionalStorageTotal(selectablePlan) | currency:'$'}}
|
||||
/{{'month' | i18n}}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.hasPremiumAccessOption && premiumAccessAddon">
|
||||
{{'premiumAccess' | i18n}}:
|
||||
{{selectablePlan.premiumAccessOptionCost | currency:'$'}} {{'monthAbbr' | i18n}} =
|
||||
{{40 | currency:'$'}}
|
||||
/{{'month' | i18n}}
|
||||
</small>
|
||||
</ng-container>
|
||||
</label>
|
||||
</div>
|
||||
<hr class="my-3">
|
||||
<div class="text-lg">
|
||||
<strong>{{'total' | i18n}}:</strong> {{total | currency:'USD $'}} /{{interval | i18n}}
|
||||
<strong>{{'total' | i18n}}:</strong> {{subtotal | currency:'USD $'}} /{{selectedPlanInterval | i18n}}
|
||||
</div>
|
||||
<ng-container *ngIf="createOrganization">
|
||||
<small class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (interval | i18n) }}</small>
|
||||
<small
|
||||
class="text-muted font-italic">{{'paymentChargedWithTrial' | i18n : (selectedPlanInterval | i18n) }}</small>
|
||||
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
|
||||
<app-payment [hideCredit]="true"></app-payment>
|
||||
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!createOrganization">
|
||||
<app-payment [showMethods]="false"></app-payment>
|
||||
</ng-container>
|
||||
<small class="text-muted font-italic mt-2 d-block" *ngIf="!createOrganization">
|
||||
{{'paymentCharged' | i18n : (interval | i18n) }}</small>
|
||||
</ng-container>
|
||||
<div [ngClass]="{'mt-4': !createOrganization || plans[plan].noPayment}">
|
||||
</div>
|
||||
<div *ngIf="singleOrgPolicyBlock" class="mt-4">
|
||||
<app-callout [type]="'error'">{{'singleOrgBlockCreateMessage' | i18n}}</app-callout>
|
||||
</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>
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
@@ -16,89 +17,196 @@ import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { CryptoService } from 'jslib/abstractions/crypto.service';
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { PolicyService } from 'jslib/abstractions/policy.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { PaymentComponent } from './payment.component';
|
||||
import { TaxInfoComponent } from './tax-info.component';
|
||||
|
||||
import { PlanType } from 'jslib/enums/planType';
|
||||
import { PolicyType } from 'jslib/enums/policyType';
|
||||
import { ProductType } from 'jslib/enums/productType';
|
||||
|
||||
import { OrganizationCreateRequest } from 'jslib/models/request/organizationCreateRequest';
|
||||
import { OrganizationUpgradeRequest } from 'jslib/models/request/organizationUpgradeRequest';
|
||||
import { PlanResponse } from 'jslib/models/response/planResponse';
|
||||
|
||||
@Component({
|
||||
selector: 'app-organization-plans',
|
||||
templateUrl: 'organization-plans.component.html',
|
||||
})
|
||||
export class OrganizationPlansComponent {
|
||||
export class OrganizationPlansComponent implements OnInit {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(TaxInfoComponent) taxComponent: TaxInfoComponent;
|
||||
|
||||
@Input() organizationId: string;
|
||||
@Input() showFree = true;
|
||||
@Input() showCancel = false;
|
||||
@Input() plan = 'free';
|
||||
@Input() product: ProductType = ProductType.Free;
|
||||
@Input() plan: PlanType = PlanType.Free;
|
||||
@Output() onSuccess = new EventEmitter();
|
||||
@Output() onCanceled = new EventEmitter();
|
||||
|
||||
selfHosted = false;
|
||||
ownedBusiness = false;
|
||||
premiumAccessAddon = false;
|
||||
storageGbPriceMonthly = 0.33;
|
||||
additionalStorage = 0;
|
||||
additionalSeats = 0;
|
||||
interval = 'year';
|
||||
loading: boolean = true;
|
||||
selfHosted: boolean = false;
|
||||
ownedBusiness: boolean = false;
|
||||
premiumAccessAddon: boolean = false;
|
||||
additionalStorage: number = 0;
|
||||
additionalSeats: number = 0;
|
||||
name: string;
|
||||
billingEmail: string;
|
||||
businessName: string;
|
||||
|
||||
storageGb: any = {
|
||||
price: 0.33,
|
||||
monthlyPrice: 0.50,
|
||||
yearlyPrice: 4,
|
||||
};
|
||||
|
||||
plans: any = {
|
||||
free: {
|
||||
basePrice: 0,
|
||||
noAdditionalSeats: true,
|
||||
noPayment: true,
|
||||
},
|
||||
families: {
|
||||
basePrice: 1,
|
||||
annualBasePrice: 12,
|
||||
baseSeats: 5,
|
||||
noAdditionalSeats: true,
|
||||
annualPlanType: PlanType.FamiliesAnnually,
|
||||
canBuyPremiumAccessAddon: true,
|
||||
},
|
||||
teams: {
|
||||
basePrice: 5,
|
||||
annualBasePrice: 60,
|
||||
monthlyBasePrice: 8,
|
||||
baseSeats: 5,
|
||||
seatPrice: 2,
|
||||
annualSeatPrice: 24,
|
||||
monthlySeatPrice: 2.5,
|
||||
monthPlanType: PlanType.TeamsMonthly,
|
||||
annualPlanType: PlanType.TeamsAnnually,
|
||||
},
|
||||
enterprise: {
|
||||
seatPrice: 3,
|
||||
annualSeatPrice: 36,
|
||||
monthlySeatPrice: 4,
|
||||
monthPlanType: PlanType.EnterpriseMonthly,
|
||||
annualPlanType: PlanType.EnterpriseAnnually,
|
||||
},
|
||||
};
|
||||
|
||||
productTypes = ProductType;
|
||||
formPromise: Promise<any>;
|
||||
singleOrgPolicyBlock: boolean = false;
|
||||
|
||||
plans: PlanResponse[];
|
||||
|
||||
constructor(private apiService: ApiService, private i18nService: I18nService,
|
||||
private analytics: Angulartics2, private toasterService: ToasterService,
|
||||
platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService,
|
||||
private router: Router, private syncService: SyncService) {
|
||||
private router: Router, private syncService: SyncService,
|
||||
private policyService: PolicyService) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.selfHosted) {
|
||||
const plans = await this.apiService.getPlans();
|
||||
this.plans = plans.data;
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
get createOrganization() {
|
||||
return this.organizationId == null;
|
||||
}
|
||||
|
||||
get selectedPlan() {
|
||||
return this.plans.find((plan) => plan.type === this.plan);
|
||||
}
|
||||
|
||||
get selectedPlanInterval() {
|
||||
return this.selectedPlan.isAnnual
|
||||
? 'year'
|
||||
: 'month';
|
||||
}
|
||||
|
||||
get selectableProducts() {
|
||||
let validPlans = this.plans.filter((plan) => plan.type !== PlanType.Custom);
|
||||
|
||||
if (this.ownedBusiness) {
|
||||
validPlans = validPlans.filter((plan) => plan.canBeUsedByBusiness);
|
||||
}
|
||||
|
||||
if (!this.showFree) {
|
||||
validPlans = validPlans.filter((plan) => plan.product !== ProductType.Free);
|
||||
}
|
||||
|
||||
validPlans = validPlans
|
||||
.filter((plan) => !plan.legacyYear
|
||||
&& !plan.disabled
|
||||
&& (plan.isAnnual || plan.product === this.productTypes.Free));
|
||||
|
||||
return validPlans;
|
||||
}
|
||||
|
||||
get selectablePlans() {
|
||||
return this.plans.filter((plan) => !plan.legacyYear && !plan.disabled && plan.product === this.product);
|
||||
}
|
||||
|
||||
additionalStoragePriceMonthly(selectedPlan: PlanResponse) {
|
||||
if (!selectedPlan.isAnnual) {
|
||||
return selectedPlan.additionalStoragePricePerGb;
|
||||
}
|
||||
return selectedPlan.additionalStoragePricePerGb / 12;
|
||||
}
|
||||
|
||||
seatPriceMonthly(selectedPlan: PlanResponse) {
|
||||
if (!selectedPlan.isAnnual) {
|
||||
return selectedPlan.seatPrice;
|
||||
}
|
||||
return selectedPlan.seatPrice / 12;
|
||||
}
|
||||
|
||||
additionalStorageTotal(plan: PlanResponse): number {
|
||||
if (!plan.hasAdditionalStorageOption) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return plan.additionalStoragePricePerGb * Math.abs(this.additionalStorage || 0);
|
||||
}
|
||||
|
||||
seatTotal(plan: PlanResponse): number {
|
||||
if (!plan.hasAdditionalSeatsOption) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return plan.seatPrice * Math.abs(this.additionalSeats || 0);
|
||||
}
|
||||
|
||||
get subtotal() {
|
||||
let subTotal = this.selectedPlan.basePrice;
|
||||
if (this.selectedPlan.hasAdditionalSeatsOption && this.additionalSeats) {
|
||||
subTotal += this.seatTotal(this.selectedPlan);
|
||||
}
|
||||
if (this.selectedPlan.hasAdditionalStorageOption && this.additionalStorage) {
|
||||
subTotal += this.additionalStorageTotal(this.selectedPlan);
|
||||
}
|
||||
if (this.selectedPlan.hasPremiumAccessOption && this.premiumAccessAddon) {
|
||||
subTotal += this.selectedPlan.premiumAccessOptionPrice;
|
||||
}
|
||||
return subTotal;
|
||||
}
|
||||
|
||||
changedProduct() {
|
||||
this.plan = this.selectablePlans[0].type;
|
||||
if (!this.selectedPlan.hasPremiumAccessOption) {
|
||||
this.premiumAccessAddon = false;
|
||||
}
|
||||
if (!this.selectedPlan.hasAdditionalStorageOption) {
|
||||
this.additionalStorage = 0;
|
||||
}
|
||||
if (!this.selectedPlan.hasAdditionalSeatsOption) {
|
||||
this.additionalSeats = 0;
|
||||
} else if (!this.additionalSeats && !this.selectedPlan.baseSeats &&
|
||||
this.selectedPlan.hasAdditionalSeatsOption) {
|
||||
this.additionalSeats = 1;
|
||||
}
|
||||
}
|
||||
|
||||
changedOwnedBusiness() {
|
||||
if (!this.ownedBusiness || this.selectedPlan.canBeUsedByBusiness) {
|
||||
return;
|
||||
}
|
||||
this.plan = PlanType.TeamsMonthly;
|
||||
}
|
||||
|
||||
changedCountry() {
|
||||
this.paymentComponent.hideBank = this.taxComponent.taxInfo.country !== 'US';
|
||||
// Bank Account payments are only available for US customers
|
||||
if (this.paymentComponent.hideBank &&
|
||||
this.paymentComponent.method === PaymentMethodType.BankAccount) {
|
||||
this.paymentComponent.method = PaymentMethodType.Card;
|
||||
this.paymentComponent.changeMethod();
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.singleOrgPolicyBlock) {
|
||||
return;
|
||||
} else {
|
||||
const policies = await this.policyService.getAll(PolicyType.SingleOrg);
|
||||
this.singleOrgPolicyBlock = policies.some(policy => policy.enabled);
|
||||
if (this.singleOrgPolicyBlock) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let files: FileList = null;
|
||||
if (this.createOrganization && this.selfHosted) {
|
||||
const fileEl = document.getElementById('file') as HTMLInputElement;
|
||||
@@ -115,7 +223,7 @@ export class OrganizationPlansComponent {
|
||||
let orgId: string = null;
|
||||
if (this.createOrganization) {
|
||||
let tokenResult: [string, PaymentMethodType] = null;
|
||||
if (!this.selfHosted && this.plan !== 'free') {
|
||||
if (!this.selfHosted && this.plan !== PlanType.Free) {
|
||||
tokenResult = await this.paymentComponent.createPaymentToken();
|
||||
}
|
||||
const shareKey = await this.cryptoService.makeShareKey();
|
||||
@@ -138,7 +246,7 @@ export class OrganizationPlansComponent {
|
||||
request.name = this.name;
|
||||
request.billingEmail = this.billingEmail;
|
||||
|
||||
if (this.plan === 'free') {
|
||||
if (this.selectedPlan.type === PlanType.Free) {
|
||||
request.planType = PlanType.Free;
|
||||
} else {
|
||||
request.paymentToken = tokenResult[0];
|
||||
@@ -146,12 +254,17 @@ export class OrganizationPlansComponent {
|
||||
request.businessName = this.ownedBusiness ? this.businessName : null;
|
||||
request.additionalSeats = this.additionalSeats;
|
||||
request.additionalStorageGb = this.additionalStorage;
|
||||
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
|
||||
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
|
||||
this.premiumAccessAddon;
|
||||
if (this.interval === 'month') {
|
||||
request.planType = this.plans[this.plan].monthPlanType;
|
||||
} else {
|
||||
request.planType = this.plans[this.plan].annualPlanType;
|
||||
request.planType = this.selectedPlan.type;
|
||||
request.billingAddressPostalCode = this.taxComponent.taxInfo.postalCode;
|
||||
request.billingAddressCountry = this.taxComponent.taxInfo.country;
|
||||
if (this.taxComponent.taxInfo.includeTaxId) {
|
||||
request.taxIdNumber = this.taxComponent.taxInfo.taxId;
|
||||
request.billingAddressLine1 = this.taxComponent.taxInfo.line1;
|
||||
request.billingAddressLine2 = this.taxComponent.taxInfo.line2;
|
||||
request.billingAddressCity = this.taxComponent.taxInfo.city;
|
||||
request.billingAddressState = this.taxComponent.taxInfo.state;
|
||||
}
|
||||
}
|
||||
const response = await this.apiService.postOrganization(request);
|
||||
@@ -162,13 +275,9 @@ export class OrganizationPlansComponent {
|
||||
request.businessName = this.ownedBusiness ? this.businessName : null;
|
||||
request.additionalSeats = this.additionalSeats;
|
||||
request.additionalStorageGb = this.additionalStorage;
|
||||
request.premiumAccessAddon = this.plans[this.plan].canBuyPremiumAccessAddon &&
|
||||
request.premiumAccessAddon = this.selectedPlan.hasPremiumAccessOption &&
|
||||
this.premiumAccessAddon;
|
||||
if (this.interval === 'month') {
|
||||
request.planType = this.plans[this.plan].monthPlanType;
|
||||
} else {
|
||||
request.planType = this.plans[this.plan].annualPlanType;
|
||||
}
|
||||
request.planType = this.selectedPlan.type;
|
||||
const result = await this.apiService.postOrganizationUpgrade(this.organizationId, request);
|
||||
if (!result.success && result.paymentIntentClientSecret != null) {
|
||||
await this.paymentComponent.handleStripeCardPayment(result.paymentIntentClientSecret, null);
|
||||
@@ -197,78 +306,4 @@ export class OrganizationPlansComponent {
|
||||
} catch { }
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.onCanceled.emit();
|
||||
}
|
||||
|
||||
changedPlan() {
|
||||
if (!this.plans[this.plan].canBuyPremiumAccessAddon) {
|
||||
this.premiumAccessAddon = false;
|
||||
}
|
||||
|
||||
if (this.plans[this.plan].monthPlanType == null) {
|
||||
this.interval = 'year';
|
||||
}
|
||||
|
||||
if (this.plans[this.plan].noAdditionalSeats) {
|
||||
this.additionalSeats = 0;
|
||||
} else if (!this.additionalSeats && !this.plans[this.plan].baseSeats &&
|
||||
!this.plans[this.plan].noAdditionalSeats) {
|
||||
this.additionalSeats = 1;
|
||||
}
|
||||
}
|
||||
|
||||
changedOwnedBusiness() {
|
||||
if (!this.ownedBusiness || this.plan === 'teams' || this.plan === 'enterprise') {
|
||||
return;
|
||||
}
|
||||
this.plan = 'teams';
|
||||
}
|
||||
|
||||
additionalStorageTotal(annual: boolean): number {
|
||||
if (annual) {
|
||||
return Math.abs(this.additionalStorage || 0) * this.storageGb.yearlyPrice;
|
||||
} else {
|
||||
return Math.abs(this.additionalStorage || 0) * this.storageGb.monthlyPrice;
|
||||
}
|
||||
}
|
||||
|
||||
seatTotal(annual: boolean): number {
|
||||
if (this.plans[this.plan].noAdditionalSeats) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (annual) {
|
||||
return this.plans[this.plan].annualSeatPrice * Math.abs(this.additionalSeats || 0);
|
||||
} else {
|
||||
return this.plans[this.plan].monthlySeatPrice * Math.abs(this.additionalSeats || 0);
|
||||
}
|
||||
}
|
||||
|
||||
baseTotal(annual: boolean): number {
|
||||
if (annual) {
|
||||
return Math.abs(this.plans[this.plan].annualBasePrice || 0);
|
||||
} else {
|
||||
return Math.abs(this.plans[this.plan].monthlyBasePrice || 0);
|
||||
}
|
||||
}
|
||||
|
||||
premiumAccessTotal(annual: boolean): number {
|
||||
if (this.plans[this.plan].canBuyPremiumAccessAddon && this.premiumAccessAddon) {
|
||||
if (annual) {
|
||||
return 40;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
get total(): number {
|
||||
const annual = this.interval === 'year';
|
||||
return this.baseTotal(annual) + this.seatTotal(annual) + this.additionalStorageTotal(annual) +
|
||||
this.premiumAccessTotal(annual);
|
||||
}
|
||||
|
||||
get createOrganization() {
|
||||
return this.organizationId == null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,17 @@
|
||||
<i class="fa fa-cog fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<ng-container *ngIf="o.useSso && o.identifier">
|
||||
<a *ngIf="o.ssoBound; else linkSso" class="dropdown-item" href="#" appStopClick
|
||||
(click)="unlinkSso(o)">
|
||||
<i class="fa fa-fw fa-chain-broken" aria-hidden="true"></i>
|
||||
{{'unlinkSso' | i18n}}
|
||||
</a>
|
||||
<ng-template #linkSso>
|
||||
<app-link-sso [organization]="o">
|
||||
</app-link-sso>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="leave(o)">
|
||||
<i class="fa fa-fw fa-sign-out" aria-hidden="true"></i>
|
||||
{{'leave' | i18n}}
|
||||
|
||||
@@ -35,6 +35,7 @@ export class OrganizationsComponent implements OnInit {
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.vault) {
|
||||
await this.syncService.fullSync(true);
|
||||
await this.load();
|
||||
}
|
||||
}
|
||||
@@ -46,6 +47,25 @@ export class OrganizationsComponent implements OnInit {
|
||||
this.loaded = true;
|
||||
}
|
||||
|
||||
async unlinkSso(org: Organization) {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
'Are you sure you want to unlink SSO for this organization?', org.name,
|
||||
this.i18nService.t('yes'), this.i18nService.t('no'), 'warning');
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.actionPromise = this.apiService.deleteSsoUser(org.id).then(() => {
|
||||
return this.syncService.fullSync(true);
|
||||
});
|
||||
await this.actionPromise;
|
||||
this.analytics.eventTrack.next({ action: 'Unlinked SSO' });
|
||||
this.toasterService.popAsync('success', null, 'Unlinked SSO');
|
||||
await this.load();
|
||||
} catch { }
|
||||
}
|
||||
|
||||
async leave(org: Organization) {
|
||||
const confirmed = await this.platformUtilsService.showDialog(
|
||||
this.i18nService.t('leaveOrganizationConfirmation'), org.name,
|
||||
|
||||
@@ -39,13 +39,15 @@
|
||||
<div id="stripe-card-expiry-element" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
<div class="form-group col-4">
|
||||
<label for="stripe-card-cvc-element" class="d-flex">
|
||||
{{'securityCode' | i18n}}
|
||||
<div class="d-flex">
|
||||
<label for="stripe-card-cvc-element">
|
||||
{{'securityCode' | i18n}}
|
||||
</label>
|
||||
<a href="https://www.cvvnumber.com/cvv.html" tabindex="-1" target="_blank" rel="noopener noreferrer"
|
||||
class="ml-auto" appA11yTitle="{{'learnMore' | i18n}}">
|
||||
<i class="fa fa-question-circle-o" aria-hidden="true"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
<div id="stripe-card-cvc-element" class="form-control stripe-form-control"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -198,8 +198,8 @@ export class PaymentComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
const handleCardPayment = () => this.showMethods ?
|
||||
this.stripe.handleCardPayment(clientSecret, this.stripeCardNumberElement) :
|
||||
this.stripe.handleCardPayment(clientSecret);
|
||||
this.stripe.handleCardSetup(clientSecret, this.stripeCardNumberElement) :
|
||||
this.stripe.handleCardSetup(clientSecret);
|
||||
return handleCardPayment().then(async (result: any) => {
|
||||
if (result.error) {
|
||||
reject(result.error.message);
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
<small class="text-muted font-italic">{{'paymentChargedAnnually' | i18n}}</small>
|
||||
<h2 class="spaced-header mb-4">{{'paymentInformation' | i18n}}</h2>
|
||||
<app-payment [hideBank]="true"></app-payment>
|
||||
<app-tax-info></app-tax-info>
|
||||
<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>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { TokenService } from 'jslib/abstractions/token.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
import { PaymentComponent } from './payment.component';
|
||||
import { TaxInfoComponent } from './tax-info.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-premium',
|
||||
@@ -24,6 +25,7 @@ import { PaymentComponent } from './payment.component';
|
||||
})
|
||||
export class PremiumComponent implements OnInit {
|
||||
@ViewChild(PaymentComponent) paymentComponent: PaymentComponent;
|
||||
@ViewChild(TaxInfoComponent) taxInfoComponent: TaxInfoComponent;
|
||||
|
||||
canAccessPremium = false;
|
||||
selfHosted = false;
|
||||
@@ -83,6 +85,8 @@ export class PremiumComponent implements OnInit {
|
||||
fd.append('paymentToken', result[0]);
|
||||
}
|
||||
fd.append('additionalStorageGb', (this.additionalStorage || 0).toString());
|
||||
fd.append('country', this.taxInfoComponent.taxInfo.country);
|
||||
fd.append('postalCode', this.taxInfoComponent.taxInfo.postalCode);
|
||||
return this.apiService.postPremium(fd);
|
||||
}).then((paymentResponse) => {
|
||||
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {
|
||||
|
||||
313
src/app/settings/tax-info.component.html
Normal file
313
src/app/settings/tax-info.component.html
Normal file
@@ -0,0 +1,313 @@
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="addressCountry">{{'country' | i18n}}</label>
|
||||
<select id="addressCountry" class="form-control" [(ngModel)]="taxInfo.country" required name="addressCountry"
|
||||
autocomplete="country" (change)="changeCountry()">
|
||||
<option value="">-- Select --</option>
|
||||
<option value="US">United States</option>
|
||||
<option value="CN">China</option>
|
||||
<option value="FR">France</option>
|
||||
<option value="DE">Germany</option>
|
||||
<option value="CA">Canada</option>
|
||||
<option value="GB">United Kingdom</option>
|
||||
<option value="AU">Australia</option>
|
||||
<option value="IN">India</option>
|
||||
<option value="-" disabled></option>
|
||||
<option value="AF">Afghanistan</option>
|
||||
<option value="AX">Åland Islands</option>
|
||||
<option value="AL">Albania</option>
|
||||
<option value="DZ">Algeria</option>
|
||||
<option value="AS">American Samoa</option>
|
||||
<option value="AD">Andorra</option>
|
||||
<option value="AO">Angola</option>
|
||||
<option value="AI">Anguilla</option>
|
||||
<option value="AQ">Antarctica</option>
|
||||
<option value="AG">Antigua and Barbuda</option>
|
||||
<option value="AR">Argentina</option>
|
||||
<option value="AM">Armenia</option>
|
||||
<option value="AW">Aruba</option>
|
||||
<option value="AT">Austria</option>
|
||||
<option value="AZ">Azerbaijan</option>
|
||||
<option value="BS">Bahamas</option>
|
||||
<option value="BH">Bahrain</option>
|
||||
<option value="BD">Bangladesh</option>
|
||||
<option value="BB">Barbados</option>
|
||||
<option value="BY">Belarus</option>
|
||||
<option value="BE">Belgium</option>
|
||||
<option value="BZ">Belize</option>
|
||||
<option value="BJ">Benin</option>
|
||||
<option value="BM">Bermuda</option>
|
||||
<option value="BT">Bhutan</option>
|
||||
<option value="BO">Bolivia, Plurinational State of</option>
|
||||
<option value="BQ">Bonaire, Sint Eustatius and Saba</option>
|
||||
<option value="BA">Bosnia and Herzegovina</option>
|
||||
<option value="BW">Botswana</option>
|
||||
<option value="BV">Bouvet Island</option>
|
||||
<option value="BR">Brazil</option>
|
||||
<option value="IO">British Indian Ocean Territory</option>
|
||||
<option value="BN">Brunei Darussalam</option>
|
||||
<option value="BG">Bulgaria</option>
|
||||
<option value="BF">Burkina Faso</option>
|
||||
<option value="BI">Burundi</option>
|
||||
<option value="KH">Cambodia</option>
|
||||
<option value="CM">Cameroon</option>
|
||||
<option value="CV">Cape Verde</option>
|
||||
<option value="KY">Cayman Islands</option>
|
||||
<option value="CF">Central African Republic</option>
|
||||
<option value="TD">Chad</option>
|
||||
<option value="CL">Chile</option>
|
||||
<option value="CX">Christmas Island</option>
|
||||
<option value="CC">Cocos (Keeling) Islands</option>
|
||||
<option value="CO">Colombia</option>
|
||||
<option value="KM">Comoros</option>
|
||||
<option value="CG">Congo</option>
|
||||
<option value="CD">Congo, the Democratic Republic of the</option>
|
||||
<option value="CK">Cook Islands</option>
|
||||
<option value="CR">Costa Rica</option>
|
||||
<option value="CI">Côte d'Ivoire</option>
|
||||
<option value="HR">Croatia</option>
|
||||
<option value="CU">Cuba</option>
|
||||
<option value="CW">Curaçao</option>
|
||||
<option value="CY">Cyprus</option>
|
||||
<option value="CZ">Czech Republic</option>
|
||||
<option value="DK">Denmark</option>
|
||||
<option value="DJ">Djibouti</option>
|
||||
<option value="DM">Dominica</option>
|
||||
<option value="DO">Dominican Republic</option>
|
||||
<option value="EC">Ecuador</option>
|
||||
<option value="EG">Egypt</option>
|
||||
<option value="SV">El Salvador</option>
|
||||
<option value="GQ">Equatorial Guinea</option>
|
||||
<option value="ER">Eritrea</option>
|
||||
<option value="EE">Estonia</option>
|
||||
<option value="ET">Ethiopia</option>
|
||||
<option value="FK">Falkland Islands (Malvinas)</option>
|
||||
<option value="FO">Faroe Islands</option>
|
||||
<option value="FJ">Fiji</option>
|
||||
<option value="FI">Finland</option>
|
||||
<option value="GF">French Guiana</option>
|
||||
<option value="PF">French Polynesia</option>
|
||||
<option value="TF">French Southern Territories</option>
|
||||
<option value="GA">Gabon</option>
|
||||
<option value="GM">Gambia</option>
|
||||
<option value="GE">Georgia</option>
|
||||
<option value="GH">Ghana</option>
|
||||
<option value="GI">Gibraltar</option>
|
||||
<option value="GR">Greece</option>
|
||||
<option value="GL">Greenland</option>
|
||||
<option value="GD">Grenada</option>
|
||||
<option value="GP">Guadeloupe</option>
|
||||
<option value="GU">Guam</option>
|
||||
<option value="GT">Guatemala</option>
|
||||
<option value="GG">Guernsey</option>
|
||||
<option value="GN">Guinea</option>
|
||||
<option value="GW">Guinea-Bissau</option>
|
||||
<option value="GY">Guyana</option>
|
||||
<option value="HT">Haiti</option>
|
||||
<option value="HM">Heard Island and McDonald Islands</option>
|
||||
<option value="VA">Holy See (Vatican City State)</option>
|
||||
<option value="HN">Honduras</option>
|
||||
<option value="HK">Hong Kong</option>
|
||||
<option value="HU">Hungary</option>
|
||||
<option value="IS">Iceland</option>
|
||||
<option value="ID">Indonesia</option>
|
||||
<option value="IR">Iran, Islamic Republic of</option>
|
||||
<option value="IQ">Iraq</option>
|
||||
<option value="IE">Ireland</option>
|
||||
<option value="IM">Isle of Man</option>
|
||||
<option value="IL">Israel</option>
|
||||
<option value="IT">Italy</option>
|
||||
<option value="JM">Jamaica</option>
|
||||
<option value="JP">Japan</option>
|
||||
<option value="JE">Jersey</option>
|
||||
<option value="JO">Jordan</option>
|
||||
<option value="KZ">Kazakhstan</option>
|
||||
<option value="KE">Kenya</option>
|
||||
<option value="KI">Kiribati</option>
|
||||
<option value="KP">Korea, Democratic People's Republic of</option>
|
||||
<option value="KR">Korea, Republic of</option>
|
||||
<option value="KW">Kuwait</option>
|
||||
<option value="KG">Kyrgyzstan</option>
|
||||
<option value="LA">Lao People's Democratic Republic</option>
|
||||
<option value="LV">Latvia</option>
|
||||
<option value="LB">Lebanon</option>
|
||||
<option value="LS">Lesotho</option>
|
||||
<option value="LR">Liberia</option>
|
||||
<option value="LY">Libya</option>
|
||||
<option value="LI">Liechtenstein</option>
|
||||
<option value="LT">Lithuania</option>
|
||||
<option value="LU">Luxembourg</option>
|
||||
<option value="MO">Macao</option>
|
||||
<option value="MK">Macedonia, the former Yugoslav Republic of</option>
|
||||
<option value="MG">Madagascar</option>
|
||||
<option value="MW">Malawi</option>
|
||||
<option value="MY">Malaysia</option>
|
||||
<option value="MV">Maldives</option>
|
||||
<option value="ML">Mali</option>
|
||||
<option value="MT">Malta</option>
|
||||
<option value="MH">Marshall Islands</option>
|
||||
<option value="MQ">Martinique</option>
|
||||
<option value="MR">Mauritania</option>
|
||||
<option value="MU">Mauritius</option>
|
||||
<option value="YT">Mayotte</option>
|
||||
<option value="MX">Mexico</option>
|
||||
<option value="FM">Micronesia, Federated States of</option>
|
||||
<option value="MD">Moldova, Republic of</option>
|
||||
<option value="MC">Monaco</option>
|
||||
<option value="MN">Mongolia</option>
|
||||
<option value="ME">Montenegro</option>
|
||||
<option value="MS">Montserrat</option>
|
||||
<option value="MA">Morocco</option>
|
||||
<option value="MZ">Mozambique</option>
|
||||
<option value="MM">Myanmar</option>
|
||||
<option value="NA">Namibia</option>
|
||||
<option value="NR">Nauru</option>
|
||||
<option value="NP">Nepal</option>
|
||||
<option value="NL">Netherlands</option>
|
||||
<option value="NC">New Caledonia</option>
|
||||
<option value="NZ">New Zealand</option>
|
||||
<option value="NI">Nicaragua</option>
|
||||
<option value="NE">Niger</option>
|
||||
<option value="NG">Nigeria</option>
|
||||
<option value="NU">Niue</option>
|
||||
<option value="NF">Norfolk Island</option>
|
||||
<option value="MP">Northern Mariana Islands</option>
|
||||
<option value="NO">Norway</option>
|
||||
<option value="OM">Oman</option>
|
||||
<option value="PK">Pakistan</option>
|
||||
<option value="PW">Palau</option>
|
||||
<option value="PS">Palestinian Territory, Occupied</option>
|
||||
<option value="PA">Panama</option>
|
||||
<option value="PG">Papua New Guinea</option>
|
||||
<option value="PY">Paraguay</option>
|
||||
<option value="PE">Peru</option>
|
||||
<option value="PH">Philippines</option>
|
||||
<option value="PN">Pitcairn</option>
|
||||
<option value="PL">Poland</option>
|
||||
<option value="PT">Portugal</option>
|
||||
<option value="PR">Puerto Rico</option>
|
||||
<option value="QA">Qatar</option>
|
||||
<option value="RE">Réunion</option>
|
||||
<option value="RO">Romania</option>
|
||||
<option value="RU">Russian Federation</option>
|
||||
<option value="RW">Rwanda</option>
|
||||
<option value="BL">Saint Barthélemy</option>
|
||||
<option value="SH">Saint Helena, Ascension and Tristan da Cunha</option>
|
||||
<option value="KN">Saint Kitts and Nevis</option>
|
||||
<option value="LC">Saint Lucia</option>
|
||||
<option value="MF">Saint Martin (French part)</option>
|
||||
<option value="PM">Saint Pierre and Miquelon</option>
|
||||
<option value="VC">Saint Vincent and the Grenadines</option>
|
||||
<option value="WS">Samoa</option>
|
||||
<option value="SM">San Marino</option>
|
||||
<option value="ST">Sao Tome and Principe</option>
|
||||
<option value="SA">Saudi Arabia</option>
|
||||
<option value="SN">Senegal</option>
|
||||
<option value="RS">Serbia</option>
|
||||
<option value="SC">Seychelles</option>
|
||||
<option value="SL">Sierra Leone</option>
|
||||
<option value="SG">Singapore</option>
|
||||
<option value="SX">Sint Maarten (Dutch part)</option>
|
||||
<option value="SK">Slovakia</option>
|
||||
<option value="SI">Slovenia</option>
|
||||
<option value="SB">Solomon Islands</option>
|
||||
<option value="SO">Somalia</option>
|
||||
<option value="ZA">South Africa</option>
|
||||
<option value="GS">South Georgia and the South Sandwich Islands</option>
|
||||
<option value="SS">South Sudan</option>
|
||||
<option value="ES">Spain</option>
|
||||
<option value="LK">Sri Lanka</option>
|
||||
<option value="SD">Sudan</option>
|
||||
<option value="SR">Suriname</option>
|
||||
<option value="SJ">Svalbard and Jan Mayen</option>
|
||||
<option value="SZ">Swaziland</option>
|
||||
<option value="SE">Sweden</option>
|
||||
<option value="CH">Switzerland</option>
|
||||
<option value="SY">Syrian Arab Republic</option>
|
||||
<option value="TW">Taiwan</option>
|
||||
<option value="TJ">Tajikistan</option>
|
||||
<option value="TZ">Tanzania, United Republic of</option>
|
||||
<option value="TH">Thailand</option>
|
||||
<option value="TL">Timor-Leste</option>
|
||||
<option value="TG">Togo</option>
|
||||
<option value="TK">Tokelau</option>
|
||||
<option value="TO">Tonga</option>
|
||||
<option value="TT">Trinidad and Tobago</option>
|
||||
<option value="TN">Tunisia</option>
|
||||
<option value="TR">Turkey</option>
|
||||
<option value="TM">Turkmenistan</option>
|
||||
<option value="TC">Turks and Caicos Islands</option>
|
||||
<option value="TV">Tuvalu</option>
|
||||
<option value="UG">Uganda</option>
|
||||
<option value="UA">Ukraine</option>
|
||||
<option value="AE">United Arab Emirates</option>
|
||||
<option value="UM">United States Minor Outlying Islands</option>
|
||||
<option value="UY">Uruguay</option>
|
||||
<option value="UZ">Uzbekistan</option>
|
||||
<option value="VU">Vanuatu</option>
|
||||
<option value="VE">Venezuela, Bolivarian Republic of</option>
|
||||
<option value="VN">Viet Nam</option>
|
||||
<option value="VG">Virgin Islands, British</option>
|
||||
<option value="VI">Virgin Islands, U.S.</option>
|
||||
<option value="WF">Wallis and Futuna</option>
|
||||
<option value="EH">Western Sahara</option>
|
||||
<option value="YE">Yemen</option>
|
||||
<option value="ZM">Zambia</option>
|
||||
<option value="ZW">Zimbabwe</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<div class="form-group">
|
||||
<label for="addressPostalCode">{{'zipPostalCode' | i18n}}</label>
|
||||
<input id="addressPostalCode" class="form-control" type="text" name="addressPostalCode"
|
||||
[(ngModel)]="taxInfo.postalCode" [required]="taxInfo.country === 'US'" autocomplete="postal-code">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6" *ngIf="organizationId && taxInfo.country !== 'US'">
|
||||
<div class="form-group form-check">
|
||||
<input class="form-check-input" id="addressIncludeTaxId" name="addressIncludeTaxId" type="checkbox"
|
||||
[(ngModel)]="taxInfo.includeTaxId">
|
||||
<label class="form-check-label" for="addressIncludeTaxId">{{'includeVAT' | i18n}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" *ngIf="organizationId && taxInfo.includeTaxId">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="taxId">{{'taxIdNumber' | i18n}}</label>
|
||||
<input id="taxId" class="form-control" type="text" name="taxId" [(ngModel)]="taxInfo.taxId">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" *ngIf="organizationId && taxInfo.includeTaxId">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="addressLine1">{{'address1' | i18n}}</label>
|
||||
<input id="addressLine1" class="form-control" type="text" name="addressLine1"
|
||||
[(ngModel)]="taxInfo.line1" autocomplete="address-line1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="addressLine2">{{'address2' | i18n}}</label>
|
||||
<input id="addressLine2" class="form-control" type="text" name="addressLine2"
|
||||
[(ngModel)]="taxInfo.line2" autocomplete="address-line2">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="addressCity">{{'cityTown' | i18n}}</label>
|
||||
<input id="addressCity" class="form-control" type="text" name="addressCity"
|
||||
[(ngModel)]="taxInfo.city" autocomplete="address-level2">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label for="addressState">{{'stateProvince' | i18n}}</label>
|
||||
<input id="addressState" class="form-control" type="text" name="addressState"
|
||||
[(ngModel)]="taxInfo.state" autocomplete="address-level1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
132
src/app/settings/tax-info.component.ts
Normal file
132
src/app/settings/tax-info.component.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
import { OrganizationTaxInfoUpdateRequest } from 'jslib/models/request/organizationTaxInfoUpdateRequest';
|
||||
import { TaxInfoUpdateRequest } from 'jslib/models/request/taxInfoUpdateRequest';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tax-info',
|
||||
templateUrl: 'tax-info.component.html',
|
||||
})
|
||||
export class TaxInfoComponent {
|
||||
@Output() onCountryChanged = new EventEmitter();
|
||||
|
||||
loading: boolean = true;
|
||||
organizationId: string;
|
||||
taxInfo: any = {
|
||||
taxId: null,
|
||||
line1: null,
|
||||
line2: null,
|
||||
city: null,
|
||||
state: null,
|
||||
postalCode: null,
|
||||
country: 'US',
|
||||
includeTaxId: false,
|
||||
};
|
||||
|
||||
private pristine: any = {
|
||||
taxId: null,
|
||||
line1: null,
|
||||
line2: null,
|
||||
city: null,
|
||||
state: null,
|
||||
postalCode: null,
|
||||
country: 'US',
|
||||
includeTaxId: false,
|
||||
};
|
||||
|
||||
constructor(private apiService: ApiService, private route: ActivatedRoute) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.parent.parent.params.subscribe(async (params) => {
|
||||
this.organizationId = params.organizationId;
|
||||
if (this.organizationId) {
|
||||
try {
|
||||
const taxInfo = await this.apiService.getOrganizationTaxInfo(this.organizationId);
|
||||
if (taxInfo) {
|
||||
this.taxInfo.taxId = taxInfo.taxId;
|
||||
this.taxInfo.state = taxInfo.state;
|
||||
this.taxInfo.line1 = taxInfo.line1;
|
||||
this.taxInfo.line2 = taxInfo.line2;
|
||||
this.taxInfo.city = taxInfo.city;
|
||||
this.taxInfo.state = taxInfo.state;
|
||||
this.taxInfo.postalCode = taxInfo.postalCode;
|
||||
this.taxInfo.country = taxInfo.country || 'US';
|
||||
this.taxInfo.includeTaxId = this.taxInfo.country !== 'US' && (
|
||||
!!taxInfo.taxId
|
||||
|| !!taxInfo.line1
|
||||
|| !!taxInfo.line2
|
||||
|| !!taxInfo.city
|
||||
|| !!taxInfo.state);
|
||||
}
|
||||
} catch { }
|
||||
} else {
|
||||
const taxInfo = await this.apiService.getTaxInfo();
|
||||
if (taxInfo) {
|
||||
this.taxInfo.postalCode = taxInfo.postalCode;
|
||||
this.taxInfo.country = taxInfo.country || 'US';
|
||||
}
|
||||
}
|
||||
this.pristine = Object.assign({}, this.taxInfo);
|
||||
// If not the default (US) then trigger onCountryChanged
|
||||
if (this.taxInfo.country !== 'US') {
|
||||
this.onCountryChanged.emit();
|
||||
}
|
||||
});
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
getTaxInfoRequest(): TaxInfoUpdateRequest {
|
||||
if (this.organizationId) {
|
||||
const request = new OrganizationTaxInfoUpdateRequest();
|
||||
request.taxId = this.taxInfo.taxId;
|
||||
request.state = this.taxInfo.state;
|
||||
request.line1 = this.taxInfo.line1;
|
||||
request.line2 = this.taxInfo.line2;
|
||||
request.city = this.taxInfo.city;
|
||||
request.state = this.taxInfo.state;
|
||||
request.postalCode = this.taxInfo.postalCode;
|
||||
request.country = this.taxInfo.country;
|
||||
return request;
|
||||
} else {
|
||||
const request = new TaxInfoUpdateRequest();
|
||||
request.postalCode = this.taxInfo.postalCode;
|
||||
request.country = this.taxInfo.country;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
submitTaxInfo(): Promise<any> {
|
||||
if (!this.hasChanged()) {
|
||||
return new Promise((resolve) => { resolve(); });
|
||||
}
|
||||
const request = this.getTaxInfoRequest();
|
||||
return this.organizationId ? this.apiService.putOrganizationTaxInfo(this.organizationId,
|
||||
request as OrganizationTaxInfoUpdateRequest) : this.apiService.putTaxInfo(request);
|
||||
}
|
||||
|
||||
changeCountry() {
|
||||
if (this.taxInfo.country === 'US') {
|
||||
this.taxInfo.includeTaxId = false;
|
||||
this.taxInfo.taxId = null;
|
||||
this.taxInfo.line1 = null;
|
||||
this.taxInfo.line2 = null;
|
||||
this.taxInfo.city = null;
|
||||
this.taxInfo.state = null;
|
||||
}
|
||||
this.onCountryChanged.emit();
|
||||
}
|
||||
|
||||
private hasChanged(): boolean {
|
||||
for (const key in this.taxInfo) {
|
||||
if (this.pristine.hasOwnProperty(key) && this.pristine[key] !== this.taxInfo[key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Directive,
|
||||
EventEmitter,
|
||||
Output,
|
||||
} from '@angular/core';
|
||||
@@ -13,6 +14,7 @@ import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
||||
import { TwoFactorProviderRequest } from 'jslib/models/request/twoFactorProviderRequest';
|
||||
|
||||
@Directive()
|
||||
export abstract class TwoFactorBaseComponent {
|
||||
@Output() onUpdated = new EventEmitter<boolean>();
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@ import { TwoFactorYubiKeyComponent } from './two-factor-yubikey.component';
|
||||
templateUrl: 'two-factor-setup.component.html',
|
||||
})
|
||||
export class TwoFactorSetupComponent implements OnInit {
|
||||
@ViewChild('recoveryTemplate', { read: ViewContainerRef }) recoveryModalRef: ViewContainerRef;
|
||||
@ViewChild('authenticatorTemplate', { read: ViewContainerRef }) authenticatorModalRef: ViewContainerRef;
|
||||
@ViewChild('yubikeyTemplate', { read: ViewContainerRef }) yubikeyModalRef: ViewContainerRef;
|
||||
@ViewChild('u2fTemplate', { read: ViewContainerRef }) u2fModalRef: ViewContainerRef;
|
||||
@ViewChild('duoTemplate', { read: ViewContainerRef }) duoModalRef: ViewContainerRef;
|
||||
@ViewChild('emailTemplate', { read: ViewContainerRef }) emailModalRef: ViewContainerRef;
|
||||
@ViewChild('recoveryTemplate', { read: ViewContainerRef, static: true }) recoveryModalRef: ViewContainerRef;
|
||||
@ViewChild('authenticatorTemplate', { read: ViewContainerRef, static: true }) authenticatorModalRef: ViewContainerRef;
|
||||
@ViewChild('yubikeyTemplate', { read: ViewContainerRef, static: true }) yubikeyModalRef: ViewContainerRef;
|
||||
@ViewChild('u2fTemplate', { read: ViewContainerRef, static: true }) u2fModalRef: ViewContainerRef;
|
||||
@ViewChild('duoTemplate', { read: ViewContainerRef, static: true }) duoModalRef: ViewContainerRef;
|
||||
@ViewChild('emailTemplate', { read: ViewContainerRef, static: true }) emailModalRef: ViewContainerRef;
|
||||
|
||||
organizationId: string;
|
||||
providers: any[] = [];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ComponentFactoryResolver,
|
||||
Directive,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
@@ -15,8 +16,9 @@ import { AddEditComponent } from '../vault/add-edit.component';
|
||||
import { MessagingService } from 'jslib/abstractions/messaging.service';
|
||||
import { UserService } from 'jslib/abstractions/user.service';
|
||||
|
||||
@Directive()
|
||||
export class CipherReportComponent {
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
|
||||
@ViewChild('cipherAddEdit', { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef;
|
||||
|
||||
loading = false;
|
||||
hasLoaded = false;
|
||||
@@ -62,6 +64,10 @@ export class CipherReportComponent {
|
||||
this.modal.close();
|
||||
await this.load();
|
||||
});
|
||||
childComponent.onRestoredCipher.subscribe(async (c: CipherView) => {
|
||||
this.modal.close();
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
|
||||
@@ -41,7 +41,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple
|
||||
async setCiphers() {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const exposedPasswordCiphers: CipherView[] = [];
|
||||
const promises: Array<Promise<void>> = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
allCiphers.forEach((c) => {
|
||||
if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') {
|
||||
return;
|
||||
|
||||
@@ -233,6 +233,9 @@
|
||||
Once syncing of your data is complete, the download icon in the top right corner will turn pink. Click
|
||||
the download icon and save the CSV file.
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'yoticsv'">
|
||||
From the Yoti browser extension, click on "Settings", then "Export Saved Logins" and save the CSV file.
|
||||
</ng-container>
|
||||
</app-callout>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
|
||||
@@ -42,7 +42,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
||||
if (this.services.size > 0) {
|
||||
const allCiphers = await this.getAllCiphers();
|
||||
const inactive2faCiphers: CipherView[] = [];
|
||||
const promises: Array<Promise<void>> = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
const docs = new Map<string, string>();
|
||||
allCiphers.forEach((c) => {
|
||||
if (c.type !== CipherType.Login || (c.login.totp != null && c.login.totp !== '') || !c.login.hasUris) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="ml-auto">
|
||||
<button class="btn btn-link" appA11yTitle="{{'copyPassword' | i18n}}"
|
||||
(click)="copy(h.password)">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { PasswordGeneratorHistoryComponent } from './password-generator-history.
|
||||
templateUrl: 'password-generator.component.html',
|
||||
})
|
||||
export class PasswordGeneratorComponent extends BasePasswordGeneratorComponent {
|
||||
@ViewChild('historyTemplate', { read: ViewContainerRef }) historyModalRef: ViewContainerRef;
|
||||
@ViewChild('historyTemplate', { read: ViewContainerRef, static: true }) historyModalRef: ViewContainerRef;
|
||||
|
||||
private modal: ModalComponent = null;
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
<div class="row" *ngIf="!editMode">
|
||||
<div class="col-6 form-group">
|
||||
<label for="type">{{'whatTypeOfItem' | i18n}}</label>
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type" class="form-control">
|
||||
<select id="type" name="Type" [(ngModel)]="cipher.type" class="form-control"
|
||||
[disabled]="cipher.isDeleted" appAutofocus>
|
||||
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -21,11 +22,12 @@
|
||||
<div class="col-6 form-group">
|
||||
<label for="name">{{'name' | i18n}}</label>
|
||||
<input id="name" class="form-control" type="text" name="Name" [(ngModel)]="cipher.name"
|
||||
required>
|
||||
required [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group" *ngIf="!organization">
|
||||
<label for="folder">{{'folder' | i18n}}</label>
|
||||
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId" class="form-control">
|
||||
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId" class="form-control"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let f of folders" [ngValue]="f.id">{{f.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -37,12 +39,12 @@
|
||||
<label for="loginUsername">{{'username' | i18n}}</label>
|
||||
<div class="input-group">
|
||||
<input id="loginUsername" class="form-control" type="text" name="Login.Username"
|
||||
[(ngModel)]="cipher.login.username" appInputVerbatim>
|
||||
<div class="input-group-append">
|
||||
[(ngModel)]="cipher.login.username" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
<div class="input-group-append" *ngIf="!cipher.isDeleted">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyUsername' | i18n}}"
|
||||
(click)="copy(cipher.login.username, 'username', 'Username')" tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,9 +52,10 @@
|
||||
<div class="col-6 form-group">
|
||||
<div class="d-flex">
|
||||
<label for="loginPassword">{{'password' | i18n}}</label>
|
||||
<div class="ml-auto d-flex">
|
||||
<div class="ml-auto d-flex" *ngIf="!cipher.isDeleted">
|
||||
<a href="#" class="d-block mr-2" appStopClick
|
||||
appA11yTitle="{{'generatePassword' | i18n}}" (click)="generatePassword()">
|
||||
appA11yTitle="{{'generatePassword' | i18n}}" (click)="generatePassword()"
|
||||
*ngIf="cipher.viewPassword">
|
||||
<i class="fa fa-lg fa-fw fa-refresh" aria-hidden="true"></i>
|
||||
</a>
|
||||
<a href="#" class="d-block" #checkPasswordBtn appStopClick
|
||||
@@ -68,18 +71,20 @@
|
||||
<div class="input-group">
|
||||
<input id="loginPassword" class="form-control text-monospace"
|
||||
type="{{showPassword ? 'text' : 'password'}}" name="Login.Password"
|
||||
[(ngModel)]="cipher.login.password" appInputVerbatim autocomplete="new-password">
|
||||
[(ngModel)]="cipher.login.password" appInputVerbatim autocomplete="new-password"
|
||||
[disabled]="cipher.isDeleted || !cipher.viewPassword">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="togglePassword()"
|
||||
tabindex="-1">
|
||||
tabindex="-1" [disabled]="!cipher.viewPassword">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyPassword' | i18n}}"
|
||||
(click)="copy(cipher.login.password, 'password', 'Password')" tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
(click)="copy(cipher.login.password, 'password', 'Password')" tabindex="-1"
|
||||
[disabled]="!cipher.viewPassword">
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,8 +93,8 @@
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="loginTotp">{{'authenticatorKeyTotp' | i18n}}</label>
|
||||
<input id="loginTotp" type="text" name="Login.Totp" class="form-control text-monospace"
|
||||
[(ngModel)]="cipher.login.totp" appInputVerbatim>
|
||||
<input id="loginTotp" type="{{cipher.viewPassword ? 'text' : 'password'}}" name="Login.Totp" class="form-control text-monospace"
|
||||
[(ngModel)]="cipher.login.totp" appInputVerbatim [disabled]="cipher.isDeleted || !cipher.viewPassword">
|
||||
</div>
|
||||
<div class="col-6 form-group totp d-flex align-items-end" [ngClass]="{'low': totpLow}">
|
||||
<div *ngIf="!cipher.login.totp || !totpCode">
|
||||
@@ -121,7 +126,7 @@
|
||||
<button type="button" class="btn btn-link"
|
||||
appA11yTitle="{{'copyVerificationCode' | i18n}}"
|
||||
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')">
|
||||
<i class="fa fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -132,7 +137,7 @@
|
||||
<label for="loginUri{{i}}">{{'uriPosition' | i18n : (i + 1)}}</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" id="loginUri{{i}}" type="text"
|
||||
name="Login.Uris[{{i}}].Uri" [(ngModel)]="u.uri"
|
||||
name="Login.Uris[{{i}}].Uri" [(ngModel)]="u.uri" [disabled]="cipher.isDeleted"
|
||||
placeholder="{{'ex' | i18n}} https://google.com" appInputVerbatim>
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
@@ -143,7 +148,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyUri' | i18n}}" (click)="copy(u.uri, 'uri', 'URI')"
|
||||
tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,19 +165,20 @@
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<select class="form-control" id="loginUriMatch{{i}}" name="Login.Uris[{{i}}].Match"
|
||||
[(ngModel)]="u.match" (change)="loginUriMatchChanged(u)">
|
||||
[(ngModel)]="u.match" (change)="loginUriMatchChanged(u)"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{o.name}}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-link text-danger ml-2" (click)="removeUri(u)"
|
||||
appA11yTitle="{{'remove' | i18n}}">
|
||||
appA11yTitle="{{'remove' | i18n}}" *ngIf="!cipher.isDeleted">
|
||||
<i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<a href="#" appStopClick (click)="addUri()" class="d-inline-block mb-3">
|
||||
<a href="#" appStopClick (click)="addUri()" class="d-inline-block mb-3" *ngIf="!cipher.isDeleted">
|
||||
<i class="fa fa-plus-circle fa-fw" aria-hidden="true"></i> {{'newUri' | i18n}}
|
||||
</a>
|
||||
</ng-container>
|
||||
@@ -182,12 +188,13 @@
|
||||
<div class="col-6 form-group">
|
||||
<label for="cardCardholderName">{{'cardholderName' | i18n}}</label>
|
||||
<input id="cardCardholderName" class="form-control" type="text"
|
||||
name="Card.CardCardholderName" [(ngModel)]="cipher.card.cardholderName">
|
||||
name="Card.CardCardholderName" [(ngModel)]="cipher.card.cardholderName"
|
||||
[disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="cardBrand">{{'brand' | i18n}}</label>
|
||||
<select id="cardBrand" class="form-control" name="Card.Brand"
|
||||
[(ngModel)]="cipher.card.brand">
|
||||
[(ngModel)]="cipher.card.brand" [disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -197,12 +204,12 @@
|
||||
<label for="cardNumber">{{'number' | i18n}}</label>
|
||||
<div class="input-group">
|
||||
<input id="cardNumber" class="form-control" type="text" name="Card.Number"
|
||||
[(ngModel)]="cipher.card.number" appInputVerbatim>
|
||||
[(ngModel)]="cipher.card.number" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyNumber' | i18n}}"
|
||||
(click)="copy(cipher.card.number, 'number', 'Number')" tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,14 +217,15 @@
|
||||
<div class="col form-group">
|
||||
<label for="cardExpMonth">{{'expirationMonth' | i18n}}</label>
|
||||
<select id="cardExpMonth" class="form-control" name="Card.ExpMonth"
|
||||
[(ngModel)]="cipher.card.expMonth">
|
||||
[(ngModel)]="cipher.card.expMonth" [disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col form-group">
|
||||
<label for="cardExpYear">{{'expirationYear' | i18n}}</label>
|
||||
<input id="cardExpYear" class="form-control" type="text" name="Card.ExpYear"
|
||||
[(ngModel)]="cipher.card.expYear" placeholder="{{'ex' | i18n}} 2019">
|
||||
[(ngModel)]="cipher.card.expYear" placeholder="{{'ex' | i18n}} 2019"
|
||||
[disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -226,7 +234,8 @@
|
||||
<div class="input-group">
|
||||
<input id="cardCode" class="form-control text-monospace"
|
||||
type="{{showCardCode ? 'text' : 'password'}}" name="Card.Code"
|
||||
[(ngModel)]="cipher.card.code" appInputVerbatim autocomplete="new-password">
|
||||
[(ngModel)]="cipher.card.code" appInputVerbatim autocomplete="new-password"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleCardCode()"
|
||||
@@ -237,7 +246,7 @@
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'securityCode' | i18n}}"
|
||||
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')" tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,7 +259,7 @@
|
||||
<div class="col-4 form-group">
|
||||
<label for="idTitle">{{'title' | i18n}}</label>
|
||||
<select id="idTitle" class="form-control" name="Identity.Title"
|
||||
[(ngModel)]="cipher.identity.title">
|
||||
[(ngModel)]="cipher.identity.title" [disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -259,107 +268,107 @@
|
||||
<div class="col-4 form-group">
|
||||
<label for="idFirstName">{{'firstName' | i18n}}</label>
|
||||
<input id="idFirstName" class="form-control" type="text" name="Identity.FirstName"
|
||||
[(ngModel)]="cipher.identity.firstName">
|
||||
[(ngModel)]="cipher.identity.firstName" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idMiddleName">{{'middleName' | i18n}}</label>
|
||||
<input id="idMiddleName" class="form-control" type="text" name="Identity.MiddleName"
|
||||
[(ngModel)]="cipher.identity.middleName">
|
||||
[(ngModel)]="cipher.identity.middleName" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idLastName">{{'lastName' | i18n}}</label>
|
||||
<input id="idLastName" class="form-control" type="text" name="Identity.LastName"
|
||||
[(ngModel)]="cipher.identity.lastName">
|
||||
[(ngModel)]="cipher.identity.lastName" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4 form-group">
|
||||
<label for="idUsername">{{'username' | i18n}}</label>
|
||||
<input id="idUsername" class="form-control" type="text" name="Identity.Username"
|
||||
[(ngModel)]="cipher.identity.username" appInputVerbatim>
|
||||
[(ngModel)]="cipher.identity.username" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idCompany">{{'company' | i18n}}</label>
|
||||
<input id="idCompany" class="form-control" type="text" name="Identity.Company"
|
||||
[(ngModel)]="cipher.identity.company">
|
||||
[(ngModel)]="cipher.identity.company" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-4 form-group">
|
||||
<label for="idSsn">{{'ssn' | i18n}}</label>
|
||||
<input id="idSsn" class="form-control" type="text" name="Identity.SSN"
|
||||
[(ngModel)]="cipher.identity.ssn" appInputVerbatim>
|
||||
[(ngModel)]="cipher.identity.ssn" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idPassportNumber">{{'passportNumber' | i18n}}</label>
|
||||
<input id="idPassportNumber" class="form-control" type="text" name="Identity.PassportNumber"
|
||||
[(ngModel)]="cipher.identity.passportNumber" appInputVerbatim>
|
||||
[(ngModel)]="cipher.identity.passportNumber" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-4 form-group">
|
||||
<label for="idLicenseNumber">{{'licenseNumber' | i18n}}</label>
|
||||
<input id="idLicenseNumber" class="form-control" type="text" name="Identity.LicenseNumber"
|
||||
[(ngModel)]="cipher.identity.licenseNumber" appInputVerbatim>
|
||||
[(ngModel)]="cipher.identity.licenseNumber" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idEmail">{{'email' | i18n}}</label>
|
||||
<input id="idEmail" class="form-control" type="text" name="Identity.Email"
|
||||
[(ngModel)]="cipher.identity.email" appInputVerbatim>
|
||||
[(ngModel)]="cipher.identity.email" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idPhone">{{'phone' | i18n}}</label>
|
||||
<input id="idPhone" class="form-control" type="text" name="Identity.Phone"
|
||||
[(ngModel)]="cipher.identity.phone">
|
||||
[(ngModel)]="cipher.identity.phone" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idAddress1">{{'address1' | i18n}}</label>
|
||||
<input id="idAddress1" class="form-control" type="text" name="Identity.Address1"
|
||||
[(ngModel)]="cipher.identity.address1">
|
||||
[(ngModel)]="cipher.identity.address1" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idAddress2">{{'address2' | i18n}}</label>
|
||||
<input id="idAddress2" class="form-control" type="text" name="Identity.Address2"
|
||||
[(ngModel)]="cipher.identity.address2">
|
||||
[(ngModel)]="cipher.identity.address2" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idAddress3">{{'address3' | i18n}}</label>
|
||||
<input id="idAddress3" class="form-control" type="text" name="Identity.Address3"
|
||||
[(ngModel)]="cipher.identity.address3">
|
||||
[(ngModel)]="cipher.identity.address3" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idCity">{{'cityTown' | i18n}}</label>
|
||||
<input id="idCity" class="form-control" type="text" name="Identity.City"
|
||||
[(ngModel)]="cipher.identity.city">
|
||||
[(ngModel)]="cipher.identity.city" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idState">{{'stateProvince' | i18n}}</label>
|
||||
<input id="idState" class="form-control" type="text" name="Identity.State"
|
||||
[(ngModel)]="cipher.identity.state">
|
||||
[(ngModel)]="cipher.identity.state" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-6 form-group">
|
||||
<label for="idPostalCode">{{'zipPostalCode' | i18n}}</label>
|
||||
<input id="idPostalCode" class="form-control" type="text" name="Identity.PostalCode"
|
||||
[(ngModel)]="cipher.identity.postalCode">
|
||||
[(ngModel)]="cipher.identity.postalCode" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 form-group">
|
||||
<label for="idCountry">{{'country' | i18n}}</label>
|
||||
<input id="idCountry" class="form-control" type="text" name="Identity.Country"
|
||||
[(ngModel)]="cipher.identity.country">
|
||||
[(ngModel)]="cipher.identity.country" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="form-group">
|
||||
<label for="notes">{{'notes' | i18n}}</label>
|
||||
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="cipher.notes"
|
||||
<textarea id="notes" name="Notes" rows="6" [(ngModel)]="cipher.notes" [disabled]="cipher.isDeleted"
|
||||
class="form-control"></textarea>
|
||||
</div>
|
||||
<h3 class="mt-4">{{'customFields' | i18n}}</h3>
|
||||
@@ -374,19 +383,19 @@
|
||||
</a>
|
||||
</div>
|
||||
<input id="fieldName{{i}}" type="text" name="Field.Name{{i}}" [(ngModel)]="f.name"
|
||||
class="form-control" appInputVerbatim>
|
||||
class="form-control" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<div class="col-7 form-group">
|
||||
<label for="fieldValue{{i}}">{{'value' | i18n}}</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="input-group" *ngIf="f.type === fieldType.Text">
|
||||
<input id="fieldValue{{i}}" class="form-control" type="text" name="Field.Value{{i}}"
|
||||
[(ngModel)]="f.value" appInputVerbatim>
|
||||
[(ngModel)]="f.value" appInputVerbatim [disabled]="cipher.isDeleted">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyValue' | i18n}}"
|
||||
(click)="copy(f.value, 'value', 'Field')" tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -394,11 +403,11 @@
|
||||
<input id="fieldValue{{i}}" type="{{f.showValue ? 'text' : 'password'}}"
|
||||
name="Field.Value{{i}}" [(ngModel)]="f.value"
|
||||
class="form-control text-monospace" appInputVerbatim
|
||||
autocomplete="new-password">
|
||||
autocomplete="new-password" [disabled]="cipher.isDeleted || (!cipher.viewPassword && !f.newField)">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'toggleVisibility' | i18n}}" (click)="toggleFieldValue(f)"
|
||||
tabindex="-1">
|
||||
tabindex="-1" [disabled]="!cipher.viewPassword && !f.newField">
|
||||
<i class="fa fa-lg" aria-hidden="true"
|
||||
[ngClass]="{'fa-eye': !f.showValue, 'fa-eye-slash': f.showValue}">
|
||||
</i>
|
||||
@@ -406,32 +415,32 @@
|
||||
<button type="button" class="btn btn-outline-secondary"
|
||||
appA11yTitle="{{'copyValue' | i18n}}"
|
||||
(click)="copy(f.value, 'value', f.type === fieldType.Hidden ? 'H_Field' : 'Field')"
|
||||
tabindex="-1">
|
||||
<i class="fa fa-lg fa-clipboard" aria-hidden="true"></i>
|
||||
tabindex="-1" [disabled]="!cipher.viewPassword && !f.newField">
|
||||
<i class="fa fa-lg fa-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-fill">
|
||||
<input id="fieldValue{{i}}" name="Field.Value{{i}}" type="checkbox"
|
||||
[(ngModel)]="f.value" *ngIf="f.type === fieldType.Boolean" appTrueFalseValue
|
||||
trueValue="true" falseValue="false">
|
||||
trueValue="true" falseValue="false" [disabled]="cipher.isDeleted">
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger ml-2" (click)="removeField(f)"
|
||||
appA11yTitle="{{'remove' | i18n}}">
|
||||
appA11yTitle="{{'remove' | i18n}}" *ngIf="!cipher.isDeleted">
|
||||
<i class="fa fa-minus-circle fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link text-muted cursor-move"
|
||||
appA11yTitle="{{'dragToSort' | i18n}}">
|
||||
appA11yTitle="{{'dragToSort' | i18n}}" *ngIf="!cipher.isDeleted">
|
||||
<i class="fa fa-bars fa-lg" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" appStopClick (click)="addField()" class="d-inline-block mb-2">
|
||||
<a href="#" appStopClick (click)="addField()" class="d-inline-block mb-2" *ngIf="!cipher.isDeleted">
|
||||
<i class="fa fa-plus-circle fa-fw" aria-hidden="true"></i> {{'newCustomField' | i18n}}
|
||||
</a>
|
||||
<div class="row">
|
||||
<div class="row" *ngIf="!cipher.isDeleted">
|
||||
<div class="col-5">
|
||||
<label for="addFieldType" class="sr-only">{{'type' | i18n}}</label>
|
||||
<select id="addFieldType" class="form-control" name="AddFieldType" [(ngModel)]="addFieldType">
|
||||
@@ -445,7 +454,8 @@
|
||||
<div class="col-5">
|
||||
<label for="organizationId">{{'whoOwnsThisItem' | i18n}}</label>
|
||||
<select id="organizationId" class="form-control" name="OrganizationId"
|
||||
[(ngModel)]="cipher.organizationId" (change)="organizationChanged()">
|
||||
[(ngModel)]="cipher.organizationId" (change)="organizationChanged()"
|
||||
[disabled]="cipher.isDeleted">
|
||||
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{o.name}}</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -459,7 +469,7 @@
|
||||
<ng-container *ngIf="collections && collections.length">
|
||||
<div class="form-check" *ngFor="let c of collections; let i = index">
|
||||
<input class="form-check-input" type="checkbox" [(ngModel)]="c.checked"
|
||||
id="collection-{{i}}" name="Collection[{{i}}].Checked">
|
||||
id="collection-{{i}}" name="Collection[{{i}}].Checked" [disabled]="cipher.isDeleted">
|
||||
<label class="form-check-label" for="collection-{{i}}">{{c.name}}</label>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -492,19 +502,20 @@
|
||||
<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>
|
||||
<span>{{(cipher?.isDeleted ? 'restore' : 'save') | i18n}}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{'cancel' | i18n}}
|
||||
</button>
|
||||
<div class="ml-auto" *ngIf="cipher">
|
||||
<button *ngIf="!organization" type="button" (click)="toggleFavorite()" class="btn btn-link"
|
||||
<button *ngIf="!organization && !cipher.isDeleted" type="button" (click)="toggleFavorite()" class="btn btn-link"
|
||||
appA11yTitle="{{(cipher.favorite ? 'unfavorite' : 'favorite') | i18n}}">
|
||||
<i class="fa fa-lg" [ngClass]="{'fa-star': cipher.favorite, 'fa-star-o': !cipher.favorite}"
|
||||
aria-hidden="true"></i>
|
||||
</button>
|
||||
<button #deleteBtn type="button" (click)="delete()" class="btn btn-outline-danger"
|
||||
appA11yTitle="{{'delete' | i18n}}" *ngIf="editMode && !cloneMode" [disabled]="deleteBtn.loading"
|
||||
appA11yTitle="{{(cipher.isDeleted ? 'permanentlyDelete' : 'delete') | i18n}}"
|
||||
*ngIf="editMode && !cloneMode" [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"
|
||||
@@ -514,4 +525,4 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
38
src/app/vault/bulk-actions.component.html
Normal file
38
src/app/vault/bulk-actions.component.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<div class="dropdown mr-2" 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)="bulkMove()" *ngIf="!deleted && !organization">
|
||||
<i class="fa fa-fw fa-share" aria-hidden="true"></i>
|
||||
{{'moveSelected' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item" appStopClick (click)="bulkShare()" *ngIf="!deleted && !organization">
|
||||
<i class="fa fa-fw fa-share-alt" aria-hidden="true"></i>
|
||||
{{'shareSelected' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item" (click)="bulkRestore()" *ngIf="deleted && !organization">
|
||||
<i class="fa fa-fw fa-undo" aria-hidden="true"></i>
|
||||
{{'restoreSelected' | i18n}}
|
||||
</button>
|
||||
<button class="dropdown-item text-danger" (click)="bulkDelete()">
|
||||
<i class="fa fa-fw fa-trash-o" aria-hidden="true"></i>
|
||||
{{(deleted ? 'permanentlyDeleteSelected' : 'deleteSelected') | 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>
|
||||
|
||||
<ng-template #bulkDeleteTemplate></ng-template>
|
||||
<ng-template #bulkRestoreTemplate></ng-template>
|
||||
<ng-template #bulkMoveTemplate></ng-template>
|
||||
<ng-template #bulkShareTemplate></ng-template>
|
||||
154
src/app/vault/bulk-actions.component.ts
Normal file
154
src/app/vault/bulk-actions.component.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
Component,
|
||||
ComponentFactoryResolver,
|
||||
Input,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
import { ToasterService } from 'angular2-toaster';
|
||||
|
||||
import { I18nService } from 'jslib/abstractions/i18n.service';
|
||||
|
||||
import { Organization } from 'jslib/models/domain/organization';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
|
||||
import { BulkDeleteComponent } from './bulk-delete.component';
|
||||
import { BulkMoveComponent } from './bulk-move.component';
|
||||
import { BulkRestoreComponent } from './bulk-restore.component';
|
||||
import { BulkShareComponent } from './bulk-share.component';
|
||||
import { CiphersComponent } from './ciphers.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vault-bulk-actions',
|
||||
templateUrl: 'bulk-actions.component.html',
|
||||
})
|
||||
export class BulkActionsComponent {
|
||||
@Input() ciphersComponent: CiphersComponent;
|
||||
@Input() modal: ModalComponent;
|
||||
@Input() deleted: boolean;
|
||||
@Input() organization: Organization;
|
||||
|
||||
@ViewChild('bulkDeleteTemplate', { read: ViewContainerRef, static: true }) bulkDeleteModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkRestoreTemplate', { read: ViewContainerRef, static: true }) bulkRestoreModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkMoveTemplate', { read: ViewContainerRef, static: true }) bulkMoveModalRef: ViewContainerRef;
|
||||
@ViewChild('bulkShareTemplate', { read: ViewContainerRef, static: true }) bulkShareModalRef: ViewContainerRef;
|
||||
|
||||
constructor(private toasterService: ToasterService,
|
||||
private i18nService: I18nService,
|
||||
private componentFactoryResolver: ComponentFactoryResolver) { }
|
||||
|
||||
bulkDelete() {
|
||||
const selectedIds = this.ciphersComponent.getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkDeleteModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkDeleteComponent>(BulkDeleteComponent, this.bulkDeleteModalRef);
|
||||
|
||||
childComponent.permanent = this.deleted;
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.organization = this.organization;
|
||||
childComponent.onDeleted.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
bulkRestore() {
|
||||
const selectedIds = this.ciphersComponent.getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkRestoreModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkRestoreComponent>(BulkRestoreComponent, this.bulkRestoreModalRef);
|
||||
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.onRestored.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
bulkShare() {
|
||||
const selectedCiphers = this.ciphersComponent.getSelected();
|
||||
if (selectedCiphers.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkShareModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkShareComponent>(BulkShareComponent, this.bulkShareModalRef);
|
||||
|
||||
childComponent.ciphers = selectedCiphers;
|
||||
childComponent.onShared.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(async () => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
bulkMove() {
|
||||
const selectedIds = this.ciphersComponent.getSelectedIds();
|
||||
if (selectedIds.length === 0) {
|
||||
this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'),
|
||||
this.i18nService.t('nothingSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.modal != null) {
|
||||
this.modal.close();
|
||||
}
|
||||
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
this.modal = this.bulkMoveModalRef.createComponent(factory).instance;
|
||||
const childComponent = this.modal.show<BulkMoveComponent>(BulkMoveComponent, this.bulkMoveModalRef);
|
||||
|
||||
childComponent.cipherIds = selectedIds;
|
||||
childComponent.onMoved.subscribe(async () => {
|
||||
this.modal.close();
|
||||
await this.ciphersComponent.refresh();
|
||||
});
|
||||
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
}
|
||||
|
||||
selectAll(select: boolean) {
|
||||
this.ciphersComponent.selectAll(select);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user